Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-05-22-shopfloor-pin-gate-design.md
gsinghpal 233e5e6e72 docs(fusion_plating_shopfloor): Phase 6 PIN gate + auto-lock spec
Sequel to the 2026-05-22 tablet redesign (Phases 1-5). Adds a tile-grid
lock screen + 4-digit PIN per tech + 5-min auto-lock + audit propagation
so multiple techs sharing one tablet get correctly-attributed actions.

Key design choices:
- 4-digit PIN (industry norm), PBKDF2-SHA256 with 200k iterations
- Per-user lockout after 5 failures (not per-tablet)
- Single Odoo session + tablet_tech_id kwarg for audit (no JS reload on
  every tech switch)
- Manager-side reset only (no SMS/email infra)
- Server-side step timer keeps running on lock (auto-pause cron is
  the upper-bound safety net)

Three sub-phases (6.1 backend / 6.2 frontend lock / 6.3 audit kwarg
propagation), each independently deployable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:48:46 -04:00

20 KiB
Raw Blame History

Shop Floor PIN Gate + Auto-Lock — Design Spec

Date: 2026-05-22 Status: Awaiting user review Phase: 6 (sequel to Phases 1-5 of 2026-05-22-shopfloor-tablet-redesign) Module owners: fusion_plating_shopfloor, fusion_plating_jobs Target client: EN Technologies (Fusion Plating)


1. Context

Phases 1-5 of the tablet redesign shipped on 2026-05-22 (entech LXC 111). They assume a single user is "logged in" to a tablet for the duration of use. Real shop floors don't work that way:

  • A single tablet sits at the EN Plating tank (or de-rack table, masking station, QC bench).
  • Multiple technicians rotate through the station during a shift.
  • A tech walks away mid-shift; the next tech walks up — and without a lock, the new tech is operating under the previous tech's identity.
  • Every step start, finish, scrap, hold, signature, and milestone advance gets attributed to the wrong person.
  • AS9100 / Nadcap audit trails break. Operators sign off on each other's work without knowing it.

The fix needs to be fast (PIN in < 2 seconds), familiar (matches iPad / debit-card UX techs already know), and silent on the timer side (locking the tablet must not pause a part in a tank).

2. Goals

  • Each tech identifies themselves with a personal 4-digit PIN.
  • Tablet auto-locks after a configurable idle period (default 5 min).
  • Quick-switch UX: tap your face tile → enter PIN → unlocked. No typing usernames.
  • All shop-floor actions (step start/finish, holds, sign-offs, milestone advances) carry the correct tech identity for audit.
  • No interruption to in-progress step timers — the server keeps counting.
  • Manager can reset a forgotten PIN; no SMS/email infrastructure required.
  • Per-station roster: a tablet at EN Plating only shows techs trained on EN Plating.

3. Non-goals (v1)

  • Multi-factor authentication (TOTP, SMS).
  • Biometric unlock (Face ID, fingerprint).
  • NFC badges or RFID readers.
  • Self-service PIN reset via email/SMS (manager-side only).
  • "Remember me" cross-device sessions.
  • Camera-based presence detection / liveness checks.

4. UX

4.1 Lock screen — tile grid

Rendered first thing when the tablet boots, after any auto-lock, and after the Hand-Off button. Replaces the current Landing/Workspace/Dashboard view entirely.

  • 3-5 tiles per row, sized for touch (~120×140 px each).
  • Sort: clocked-in techs first, alphabetical within bucket.
  • Each tile: avatar + name + small green dot if clocked in.
  • A station with x_fc_authorised_user_ids configured shows only those techs; otherwise all techs in the operator group.
  • "Other..." chip at the end opens a username search for off-roster cases (cross-trained tech covering an unfamiliar station).
  • Tap a tile → PIN pad slides up as a modal.

4.2 PIN pad

  • Numeric 0-9 in a 3×4 grid + Clear + Submit.
  • 4 dot placeholders fill as digits are typed.
  • Auto-submit on the 4th digit (no Enter required).
  • Wrong PIN → quick shake animation (CSS keyframe), dots clear, tile grid stays.
  • After 5 sequential failures for one tech, that tech is locked for 5 minutes. Other techs can still unlock the tablet.
  • "Forgot?" link surfaces a friendly message: "Ask a manager to reset your PIN."

4.3 Hand-Off button

  • Top-right corner of every authenticated view (Landing, Workspace, Manager Dashboard), next to QR scan.
  • Big icon + label: 🔒 Hand Off.
  • Tap → confirm dialog "Lock this tablet now?" → instant lock to tile grid.
  • Confirm dialog prevents accidental locks; can be skipped in v2 with rapid double-tap.

4.4 Idle warning

  • At 30 seconds before auto-lock, a yellow pulsing border appears around the entire viewport.
  • A toast slides in: "Locking in 28s · tap anywhere to stay".
  • Countdown decrements in 1s ticks until 0.
  • Any pointer/touch event clears the warning and resets the timer.
  • At 0s, the tile grid replaces the current view.

4.5 Session continuity (state preservation on lock)

State On lock On unlock
In-progress step timer Server-side timer keeps running. No pause event fired. Resumes accurate elapsed time.
OWL state (scroll, expanded step) Preserved in memory Restored
HoldComposer modal open Preserved (dialog still mounted under the lock overlay) Available immediately
SignaturePad open mid-stroke Thrown away. Signature flow restarts. Fresh signature required.
QR scan drawer open Preserved Available
Refresh interval (15s/8s polling) Paused Resumed

4.6 Profile preferences — set / change PIN

  • New "Tablet PIN" group on res.users preferences (user-facing form).
  • Single button: Set Tablet PIN or Change PIN (label flips depending on whether a hash exists).
  • Tapping it opens a modal with 3 PIN inputs:
    • Current PIN (only if a PIN is set; skipped on first-time set)
    • New PIN
    • Confirm new PIN
  • All three use the same FpPinPad component as the unlock screen.
  • Subtext shows "Last changed: 2026-05-22" or "Cleared by manager".

4.7 Manager-side reset

  • New header button on res.users form: Reset Tablet PIN (visible only to group_fusion_plating_manager and above).
  • Tap → confirm dialog → posts to user's chatter ("Tablet PIN reset by Manager X on 2026-05-22") + clears the hash.
  • Tech sets a new PIN on next unlock attempt.

5. Backend

5.1 Model fields on res.users (in fusion_plating_shopfloor/models/res_users.py — new file)

Field Type Notes
x_fc_tablet_pin_hash Char Hash (SHA-256 + per-user salt) of the PIN. Stored as <salt>$<hash>. groups='fusion_plating.group_fusion_plating_manager' — non-manager users cannot even read other users' hash field.
x_fc_tablet_pin_set_date Datetime When the current hash was set. NULL if PIN was cleared by manager.
x_fc_tablet_pin_failed_count Integer (0) Sequential failed attempts since last success. Resets to 0 on a correct PIN.
x_fc_tablet_locked_until Datetime Lockout expiry. NULL when not locked.

5.2 Extras on fusion.plating.shopfloor.station

Field Type Notes
x_fc_authorised_user_ids Many2many → res.users If non-empty, the tile grid restricts to these users. Empty = "all operators".
x_fc_idle_lock_minutes Integer, nullable Per-station override; null = use global default.

5.3 ir.config_parameter defaults

Key Default Purpose
fp.shopfloor.tablet_idle_lock_minutes 5 Global idle threshold
fp.shopfloor.tablet_pin_fail_threshold 5 Failures before lockout
fp.shopfloor.tablet_pin_fail_lockout_minutes 5 Lockout duration
fp.shopfloor.tablet_warn_seconds_before_lock 30 When the yellow border appears

5.4 HTTP endpoints (/fp/tablet/*)

All type='jsonrpc', auth='user'. Auth = the tablet's persistent Odoo session (a "shopfloor service" account or any non-locked user).

Endpoint Body Returns
POST /fp/tablet/tiles {station_id?} {ok, tiles: [{user_id, name, avatar_url, is_clocked_in, has_pin}, ...]}. Respects station.x_fc_authorised_user_ids.
POST /fp/tablet/unlock {user_id, pin} {ok: true, current_tech_id, current_tech_name} or {ok: false, error, locked_until?, attempts_remaining}.
POST /fp/tablet/set_pin {old_pin?, new_pin} Caller's PIN only. old_pin required if a hash exists. {ok, error?}.
POST /fp/tablet/reset_pin_for {user_id} Manager-only; manager group enforced server-side. Clears target user's hash + posts chatter.
POST /fp/tablet/ping {current_tech_id} Bumps a server-side "last active" timestamp for forensics. Called on every successful tech action.

5.5 Hash algorithm

import hashlib, secrets

def _hash_pin(pin: str, salt: bytes = None) -> str:
    salt = salt or secrets.token_bytes(16)
    digest = hashlib.pbkdf2_hmac('sha256', pin.encode('utf-8'), salt, 200_000)
    return f"{salt.hex()}${digest.hex()}"

def _verify_pin(pin: str, stored: str) -> bool:
    salt_hex, expected_hex = stored.split('$', 1)
    salt = bytes.fromhex(salt_hex)
    digest = hashlib.pbkdf2_hmac('sha256', pin.encode('utf-8'), salt, 200_000)
    return digest.hex() == expected_hex

200,000 PBKDF2 iterations gives ~50ms verify time on entech-class hardware — fast enough for tech UX, slow enough to make a brute-force attack expensive even with the database stolen.

5.6 Audit propagation (Phase 6.3)

All existing shop-floor endpoints that take an action (/fp/shopfloor/start_wo, stop_wo, bump_qty_done, bump_qty_scrapped, log_chemistry, log_thickness_reading, quality_hold, mark_gate, start_bake, end_bake, /fp/workspace/{hold,sign_off,advance_milestone}) gain an optional tablet_tech_id kwarg.

When the OWL component passes tablet_tech_id:

  • Server verifies the id corresponds to a recent successful /fp/tablet/unlock (within session's idle window).
  • All chatter posts use that user's name instead of env.uid.
  • All writes to records set create_uid / write_uid to that user (via with_user(...) context manager).
  • If tablet_tech_id is missing or stale, server falls back to env.uid (the tablet's session user) for back-compat.

This keeps the audit trail honest without forcing a full Odoo session swap on every PIN unlock (which would clear all OWL state and JS bundle cache).

6. Frontend architecture

6.1 New OWL components

Component File Purpose
FpTabletLock static/src/js/tablet_lock.js Top-level wrapper around Landing/Workspace/Manager. Renders tile grid when locked; renders children when unlocked.
FpPinPad static/src/js/components/pin_pad.js Numeric pad modal. Used by FpTabletLock unlock AND Profile set-PIN flow.
FpPinSetup static/src/js/components/pin_setup.js Modal for set/change PIN. Wraps 3 instances of FpPinPad (old + new + confirm).
FpIdleWarning static/src/js/components/idle_warning.js Yellow-border + countdown toast component shown at T-30s.

6.2 Activity tracker service

  • Registered as fp_shopfloor_activity in the OWL services registry.
  • Tracks lastActiveAt (epoch ms).
  • Listens at document level: pointerdown, touchstart, keydown, visibilitychange.
  • Public API: bumpActivity(), getSecondsUntilLock(), subscribe(cb), lock().
  • Bumps server-side on every ping (debounced to once per 30s).

6.3 Auto-lock flow

// inside FpTabletLock setup()
this.activity = useService("fp_shopfloor_activity");
this._tick = setInterval(() => {
    const remaining = this.activity.getSecondsUntilLock();
    if (remaining <= 0) {
        this.state.locked = true;
        this.state.currentTechId = null;
    } else if (remaining <= this.warnThresholdSec) {
        this.state.idleWarning = remaining;
    } else if (this.state.idleWarning) {
        this.state.idleWarning = null;  // user tapped, reset
    }
}, 1000);

6.4 RPC plumbing

A tiny client-side helper wraps rpc() so every shop-floor call automatically includes tablet_tech_id:

// fp_rpc.js
import { rpc as baseRpc } from "@web/core/network/rpc";
import { registry } from "@web/core/registry";

const techStore = registry.category("services").get("fp_shopfloor_tech_store");

export function fpRpc(url, params = {}) {
    if (techStore.currentTechId) {
        params = { ...params, tablet_tech_id: techStore.currentTechId };
    }
    return baseRpc(url, params);
}

Landing, Workspace, Manager Dashboard switch from rpc(...) to fpRpc(...) for action calls. Read-only calls (load, tiles, kanban) don't need the kwarg.

6.5 Component composition

FpTabletLock (NEW outer wrapper, mounted by every client action)
├── if locked → FpPinPad (tile grid + entry)
├── if idle warning → FpIdleWarning overlay
└── else → existing client action (Landing | Workspace | Manager Dashboard)
        + Hand-Off button injected into existing headers

The "tablet locked" boolean lives in a shared OWL service (fp_shopfloor_tech_store) — every client action checks it on mount and subscribes for changes.

7. Edge cases

Case Handling
No tech has set a PIN yet Tile shows "PIN required" overlay. Tap tile → guided "you must set a PIN before using this tablet" → set-PIN flow → unlock.
Manager just reset a tech's PIN Tile still shows; tap → "PIN was cleared by a manager — set a new one" → set-PIN flow → unlock.
Tablet boots with no station paired Tile grid shows + a "Pair this station" CTA. Station QR scan works before any tech is logged in.
Network drop mid-unlock Spinner + Retry button after 5s. Backend tolerates duplicate unlocks (idempotent on success — counter just stays at 0).
Tech mid-step when tablet locks Step timer keeps running on server. Auto-pause cron (Phase 2) is the upper-bound safety net.
Tech A's PIN locked for 5 min — can tech B unlock? Yes. Lockout is per-user, not per-tablet.
Tech keeps tablet active by setting a heavy weight on it Activity = pointer/touch/key events only, not mouse-move. A weight doesn't fire those events. Still locks after 5 min.
Tech is mid-RPC when lock fires RPC completes (server keeps running). Response is dropped silently — UI is already showing the tile grid.
Two tabs / windows on the same browser Each tab has its own FpTabletLock state. They lock independently. Acceptable for v1; not a real shop scenario.
Manager wants to act AS a tech Out of scope. Manager unlocks with their own PIN; their actions carry their own uid.

8. Testing

8.1 Python tests (fusion_plating_shopfloor/tests/test_tablet_pin.py)

Test Verifies
test_set_pin_first_time User with no hash can set PIN; resulting hash is salted and length > 32.
test_set_pin_change_requires_old Setting a new PIN when one exists requires correct old_pin; wrong old_pin rejected.
test_unlock_correct_pin_resets_failure_count Failed → failed → correct → counter is 0.
test_unlock_5_wrong_locks_user 5 wrong attempts → 6th returns locked_until. 7th still rejected.
test_lockout_expires_after_threshold After 5 min sim time elapsed → next attempt allowed again.
test_reset_pin_for_requires_manager Operator → AccessError. Supervisor → AccessError. Manager → success.
test_reset_pin_clears_hash_and_posts_chatter After reset: hash is False, set_date is False, chatter has "PIN reset by Manager X".
test_tiles_filtered_by_station_roster Station with authorised_user_ids → tiles is subset. Empty list → all operator-group users.
test_audit_kwarg_used_in_step_finish RPC with tablet_tech_id=N → step's write_uid == N (not env.uid).
test_audit_kwarg_invalid_falls_back_to_session Invalid tablet_tech_id → write_uid == env.uid, no error.

8.2 Manual QA

docs/qa/2026-05-22-shopfloor-pin-gate-qa.md walkthrough:

  1. Tech A sets PIN via Preferences
  2. Tech A unlocks tablet → starts a step
  3. 5 min idle elapses → tablet locks
  4. Tech B unlocks → finishes Tech A's step
  5. Audit chatter shows: started by A at T+0, finished by B at T+6
  6. Manager taps Reset PIN on Tech A's res.users form
  7. Tech A unlocks → set-PIN flow
  8. Tech A fails PIN 5 times → lockout kicks in
  9. Tech A waits 5 min → unlocks successfully

9. Build sequence (3 sub-phases)

Each ships independently and can be rolled back independently.

Sub-phase Ships Independently deployable?
6.1 — Backend model fields on res.users + station extras + ir.config_parameter defaults + 5 /fp/tablet/* endpoints + Profile prefs Set/Change PIN button + Manager Reset PIN button on res.users form Yes — works silently behind the scenes. Techs can set PINs but the lock screen doesn't render yet.
6.2 — Frontend lock screen FpTabletLock wrapper + FpPinPad + FpIdleWarning + activity tracker service + Hand-Off button injection into existing headers Yes — lock screen goes live. Audit credit still defaults to tablet session user without 6.3.
6.3 — Audit propagation tablet_tech_id optional kwarg on all existing action endpoints + fpRpc() wrapper + Landing/Workspace/Manager updated to use it Yes — refines the audit trail. Without it, actions are recorded against the tablet's session uid.

10. Backwards compatibility

  • Any tablet that hasn't been upgraded to Phase 6.2 continues to work unauthenticated (no lock screen). Once 6.2 lands, ALL tablets start showing the lock screen.
  • Endpoints from Phases 1-5 keep their existing signatures. tablet_tech_id is purely additive.
  • Setting / changing the PIN is opt-in per user. A tech without a PIN sees a "set one to continue" prompt; they can't dismiss it.
  • No model migration required — all new fields default to NULL.
  • ir.config_parameter defaults are read at runtime, no install-time setup needed.

11. Rollback strategy

Sub-phase Rollback
6.1 Disable endpoints in controllers/__init__.py. Model fields are additive, safe to drop.
6.2 Hide FpTabletLock via a feature flag (ir.config_parameter fp.shopfloor.tablet_lock_enabled, default true; set false to bypass). Existing client actions render directly again.
6.3 Stop sending tablet_tech_id from fpRpc() — server falls back to env.uid.

12. Out of scope for v1

  • Biometric (Face ID, fingerprint)
  • NFC badges
  • TOTP / SMS / email-based reset
  • "Remember me" cross-device sessions
  • Per-tech idle threshold (only per-station + global)
  • Lock-screen widgets (weather, time, KPIs) — keep the tile grid focused
  • Camera-based presence / liveness
  • Pre-fetched tile grid (each unlock call fetches fresh)
  • Different PIN lengths per tech (4 digits for everyone)

13. Decisions log

Decision Rationale
4-digit PIN over 6-digit Speed. Industry norm. Lockout + per-user-fail-counter makes 10,000 combos secure enough.
PBKDF2-SHA256, 200k iterations ~50ms verify on entech hardware. Safe against rainbow tables; brute-force-resistant even with DB stolen.
Hash field is manager-readable only Operators can't even view other users' hash. Reduces lateral attack surface.
Per-user lockout, not per-tablet A bad-actor wrong-PIN'ing one user shouldn't deny service to other techs on the same tablet.
5 minute idle default Compromise: long enough for legitimate idle-watching of a tank, short enough that a walk-away is caught. Configurable per-station.
Server-side step timer keeps running on lock Locking is UI; nothing should pause physical processes. Auto-pause cron is the deeper safety net.
Single Odoo session, PIN overlay credits via tablet_tech_id kwarg No JS bundle reload, no state loss, no flicker. Audit kwarg keeps the trail honest.
Manager-only reset (no self-service) Plating shops rarely have per-tech email/SMS. Manager is always present. Lower infra.
30s warning before lock Compromise: catches "I was right there" cases without being annoyingly chatty.
tablet_tech_id is opt-in additive kwarg Lets 6.3 ship after 6.2 without breaking anything; lets older callers continue working unchanged.

14. Future v2 candidates

  • NFC badge tap (cheap USB readers, ~$30)
  • Personal QR badge on lanyard (no hardware beyond what we already have)
  • Per-tech idle threshold (long-shift senior techs vs cross-trained probationers)
  • Lock-screen KPIs (shop output today, hot WOs visible without unlocking)
  • "Switch tech without re-PIN" — keep both signed in for hand-off audit on the same step
  • Mobile app companion with biometric unlock

Next step: user reviews this spec. Once approved, transition to superpowers:writing-plans to produce the phased implementation plan.