From 233e5e6e72058977392ba37392b7663459647438 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Fri, 22 May 2026 23:48:46 -0400 Subject: [PATCH] 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) --- .../2026-05-22-shopfloor-pin-gate-design.md | 358 ++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 fusion_plating/docs/superpowers/specs/2026-05-22-shopfloor-pin-gate-design.md diff --git a/fusion_plating/docs/superpowers/specs/2026-05-22-shopfloor-pin-gate-design.md b/fusion_plating/docs/superpowers/specs/2026-05-22-shopfloor-pin-gate-design.md new file mode 100644 index 00000000..e941075a --- /dev/null +++ b/fusion_plating/docs/superpowers/specs/2026-05-22-shopfloor-pin-gate-design.md @@ -0,0 +1,358 @@ +# 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 `$`. `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 + +```python +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 + +```js +// 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`: + +```js +// 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.