Compare commits

...

8 Commits

Author SHA1 Message Date
gsinghpal
a6546ac858 docs(fusion_plating_shopfloor): implementation plan for Phase 6 PIN gate
3-sub-phase TDD plan executing the spec at
docs/superpowers/specs/2026-05-22-shopfloor-pin-gate-design.md:

- Phase 6.1 (Backend): res.users PIN fields + PBKDF2-SHA256 hash
  helpers, 5 /fp/tablet/* endpoints (tiles/unlock/set_pin/reset_pin_for/
  ping), per-user lockout after 5 failures, station roster +
  idle-override fields, ir.config_parameter defaults, Preferences
  Set/Change PIN button, manager Reset PIN header button. Tests
  cover hash safety, lockout edge cases, manager-only enforcement,
  tile filtering.

- Phase 6.2 (Frontend lock screen): tech_store + activity_tracker
  OWL services, FpPinPad + FpIdleWarning + FpPinSetup components,
  FpTabletLock outer wrapper, wire into Landing/Workspace/Manager
  Dashboard with Hand-Off button injection.

- Phase 6.3 (Audit propagation): fpRpc wrapper auto-injects
  tablet_tech_id, env_for_tablet_tech server helper, all action
  endpoints (workspace + shopfloor + manager) accept the kwarg and
  rebind env via env.with_user() so writes carry the right operator.

Each sub-phase ships independently per spec §9. Plan follows the
established workflow: write tests + commit, verify on entech (local
docker doesn't have fusion_plating mounted).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 00:05:45 -04:00
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
gsinghpal
b06a5b2d12 fix(fusion_plating_jobs): delete orphan Plant Overview menu record
Phase 3 removed the menu_fp_shopfloor_plant_overview menuitem from
fp_menu.xml, but Odoo doesn't auto-delete orphan records when XML
disappears — the menu stayed in the database. Combined with P3.5's
action retarget (action_fp_plant_overview tag → fp_shopfloor_landing),
clicking it landed on the same Landing component as Workstation —
hence the duplicate menu items both opening the same screen.

Adds <delete model='ir.ui.menu' id='...'> in legacy_menu_hide.xml so
future -u runs scrub the orphan. Drops the now-defunct group_ids
block for the deleted menu. The action record stays (bookmark
back-compat).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:07:34 -04:00
gsinghpal
3ef67c6beb fix(fusion_plating_shopfloor): import fields in manager_controller
The Phase 4 endpoints (/fp/manager/funnel, approval_inbox, at_risk)
all use fields.Datetime.now() but the controller only imported http
+ request. Hitting the Workflow Funnel tab on Manager Desk threw:

  NameError: name 'fields' is not defined

Funnel auto-loads on dashboard mount → infinite spinner + 'Funnel:
Odoo Server Error' notification. Same bug would have hit at_risk
and approval_inbox on first navigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:01:44 -04:00
gsinghpal
4a304e02f3 fix(fusion_plating_jobs): restore quick_look_qty + instruction_attachment_ids
Same regression as the previous commit — b0070afc removed all 4 quick_look
related fields, my first fix only caught 2 of them. Restoring the remaining
2 so the quick-look view fully validates.
2026-05-22 22:47:43 -04:00
gsinghpal
0d08d2d135 fix(fusion_plating_jobs): restore quick_look_partner_id + quick_look_part_catalog_id
Commit b0070afc removed these two related fields from fp.job.step but
the view fp_job_step_quick_look_views.xml still references them. The
mismatch was dormant because entech never ran -u between b0070afc
and the 2026-05-22 deploy. Re-running -u during the Phase 1-4 deploy
caught it:

  Field "quick_look_part_catalog_id" does not exist in model
  "fp.job.step"

Restoring both as related fields (zero-cost, fixes the view without
touching XML).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:46:18 -04:00
gsinghpal
f9cb1b11ce fix(fusion_plating_jobs): drop ir.cron numbercall/doall — removed in Odoo 19
Caught during entech deploy of the Phase 2 auto-pause cron. Odoo 19
ir.cron no longer accepts numbercall or doall fields; the load fails
with:

  ValueError: Invalid field 'numbercall' in 'ir.cron'

Removed both from ir_cron_autopause_stale_steps. The other crons in
the same file (nudge stale paused / in_progress) already used the
minimal field set — matching that pattern now.

Also added a CLAUDE.md section so future-Claude doesn't reintroduce
the speculative fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:43:34 -04:00
gsinghpal
1122f84007 docs(fusion_plating): document tablet redesign architecture in CLAUDE.md (P5.1)
Some checks failed
fusion_accounting CI / test (fusion_accounting_ai) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_core) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_migration) (push) Has been cancelled
Replaces the stale "Plant Overview Dashboard" section with a current
"Shop Floor Architecture" section covering the Phase 1-4 deliverables:

  - 3 OWL client actions (Landing / JobWorkspace / Manager Dashboard)
  - 5 shared OWL services
  - Backend endpoints (workspace / landing / manager)
  - Auto-pause cron config knob (ir.config_parameter name)
  - Key new model fields with their purpose
  - Operator ACL lift summary
  - Deprecated-but-still-live legacy surfaces (Phase 5 cleanup pending)
  - Old patterns to avoid

Links to the spec + plan docs as the authoritative reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:23:23 -04:00
7 changed files with 3018 additions and 14 deletions

View File

@@ -166,6 +166,20 @@ These modules have **source code in this repo** but are **intentionally NOT inst
| `fusion_plating_culture` | `state=uninstalled`, dir removed from entech disk | Soft people-ops feature (peer kudos / "Fundamental of the Week"); zero data entered; not a client priority. Top-level "Culture" menu confused operators. | Ask the client whether they want it before reinstalling. If yes: re-sync folder + `-i fusion_plating_culture` + seed a value set. | | `fusion_plating_culture` | `state=uninstalled`, dir removed from entech disk | Soft people-ops feature (peer kudos / "Fundamental of the Week"); zero data entered; not a client priority. Top-level "Culture" menu confused operators. | Ask the client whether they want it before reinstalling. If yes: re-sync folder + `-i fusion_plating_culture` + seed a value set. |
| `fusion_plating_sensors` | deleted entirely (not in repo anymore) | Duplicated `fusion_plating_iot`'s scope but with no working alerting logic. Its valuables (sensor_type taxonomy, dashboard, location flexibility) were ported into `fusion_iot/fusion_plating_iot/`. | N/A — gone. Any new sensor work goes in `fusion_iot/fusion_plating_iot/`. | | `fusion_plating_sensors` | deleted entirely (not in repo anymore) | Duplicated `fusion_plating_iot`'s scope but with no working alerting logic. Its valuables (sensor_type taxonomy, dashboard, location flexibility) were ported into `fusion_iot/fusion_plating_iot/`. | N/A — gone. Any new sensor work goes in `fusion_iot/fusion_plating_iot/`. |
## Removing menus/records — Odoo does NOT auto-delete orphans
Deleting a `<menuitem>` (or any `<record>`) from a data XML file does NOT remove the corresponding database row. The XML loader only updates records it sees; orphans persist in `ir.ui.menu` / `ir.model.data` until you delete them explicitly. Symptom: the menu still appears in the UI after `-u`. Fix — add a `<delete>` directive in a data file with `noupdate="0"`:
```xml
<delete model="ir.ui.menu" id="module_name.menu_xmlid_to_remove"/>
```
Caught 2026-05-22 when the Phase 3 Plant Overview menu kept showing alongside the new Workstation menu after deploy.
## Odoo 19 ir.cron — `numbercall` and `doall` are gone
The legacy `numbercall=-1` (run-forever) and `doall=False` (catch-up-missed) fields were removed from `ir.cron` in Odoo 19. Including them in `<record model="ir.cron">` data XML produces:
```
ValueError: Invalid field 'numbercall' in 'ir.cron'
```
Use only: `name`, `model_id`, `state`, `code` (or `function`/`model`), `interval_number`, `interval_type`, `active`. Caught during the 2026-05-22 entech deploy of the auto-pause cron.
## Critical Rules — Odoo 19 ## Critical Rules — Odoo 19
1. **NEVER code from memory** — Read reference files from the server first. 1. **NEVER code from memory** — Read reference files from the server first.
2. **Backend OWL**: `static template`, `static props = ["*"]`, standalone `rpc()` from `@web/core/network/rpc`. NOT `useService("rpc")`. 2. **Backend OWL**: `static template`, `static props = ["*"]`, standalone `rpc()` from `@web/core/network/rpc`. NOT `useService("rpc")`.
@@ -339,13 +353,57 @@ POST /fp/recipe/duplicate — deep-copy recipe
### Client Recipes Created ### Client Recipes Created
- `ENP-ALUM-BASIC` — Electroless Nickel Plating Aluminium Basic (9 operations, 15 steps). Data file: `fusion_plating/data/fp_recipe_enp_alum_basic.xml` - `ENP-ALUM-BASIC` — Electroless Nickel Plating Aluminium Basic (9 operations, 15 steps). Data file: `fusion_plating/data/fp_recipe_enp_alum_basic.xml`
## Plant Overview Dashboard ## Shop Floor Architecture (2026-05-22 tablet redesign — Phases 1-4)
- OWL client action: `fp_plant_overview` in `fusion_plating_shopfloor`
- Kanban columns = work centres, cards = active `mrp.workorder` records Spec: [docs/superpowers/specs/2026-05-22-shopfloor-tablet-redesign-design.md](docs/superpowers/specs/2026-05-22-shopfloor-tablet-redesign-design.md)
- Drag & drop between columns (writes `workcenter_id` on the work order) Plan: [docs/superpowers/plans/2026-05-22-shopfloor-tablet-redesign-plan.md](docs/superpowers/plans/2026-05-22-shopfloor-tablet-redesign-plan.md)
- Endpoint: `POST /fp/shopfloor/plant_overview`
- Move endpoint: `POST /fp/shopfloor/plant_overview/move_card` **Three OWL client actions** (registered under `registry.category("actions")`):
- Auto-refreshes every 30s - `fp_shopfloor_landing` — Workstation kanban entry. Station-scoped or All-Plant mode toggle. Tap a card → JobWorkspace. Replaces the legacy `fp_shopfloor_tablet` and folds in `fp_plant_overview`.
- `fp_job_workspace` — Full-screen single-WO surface. Sticky header (WO #, customer, qty, workflow chip), sticky 9-stage workflow bar, step list with GateViz blockers, side panel (spec/attachments/chatter), sticky action rail (Hold/Note/Milestone). Opens from kanban tap, smart button, QR scan, or manager card tap.
- `fp_manager_dashboard` — Manager Desk with 4 sibling tabs: **Workflow Funnel** (default), **Approval Inbox**, **Plant Board** (existing 3-column), **At-Risk** (trending late + hold reasons + bottleneck heatmap).
**Five shared OWL services** in `fusion_plating_shopfloor/static/src/js/components/`:
- `WorkflowChip` — workflow.state pill + optional next-action hint
- `GateViz` — "Can't start because…" explainer (reads `fp.job.step.blocker_kind`/`reason`)
- `FpSignaturePad` — Dialog canvas signature capture
- `FpHoldComposer` — Dialog hold-create form with reason picker + qty + photo
- `FpKanbanCard` — standard WO/step card with embedded WorkflowChip + blocker badge
**Backend endpoints (Phase 1-4):**
- Workspace: `/fp/workspace/{load,hold,sign_off,advance_milestone}`
- Landing: `/fp/landing/kanban` (mode=station|all_plant)
- Manager: `/fp/manager/{overview,funnel,approval_inbox,at_risk}`
**Auto-pause cron — fixes 411-hour ghost timers:**
- `_cron_autopause_stale_steps()` runs every 30 min
- Threshold from `ir.config_parameter` **`fp.shopfloor.autopause_threshold_hours`** (default 8.0)
- Recipe nodes opt out via `fusion.plating.process.node.long_running=True` for 24h bakes etc.
- Flips `state=in_progress` idle > threshold to `paused` with chatter audit
**Key model fields added (Phase 1-4):**
- `fp.job.display_wo_name` — "WO # 00001" formatter for tablet/dashboard (model `name` field stays `WH/JOB/…`; system-wide sequence rename deferred)
- `fp.job.late_risk_ratio` — stored Float (remaining_planned / minutes_to_deadline) driving At-Risk view
- `fp.job.active_step_id` — computed M2o to currently in_progress step
- `fp.job.step.blocker_kind` / `blocker_reason` / `blocker_jump_target_*` — drives GateViz
- `fp.work.centre.bottleneck_score` / `avg_wait_minutes` — drives At-Risk bottleneck heatmap
**Operator ACL lift (per "techs wear multiple hats" rule):**
- `fp.certificate` — operator gained write (flip draft → issued from tablet)
- `fp.thickness.reading` — operator gained create+write (Fischerscope capture from tablet)
- `fp.job.node.override` — operator gained read (see opt-out badges on steps)
- Supervisor-only ops (step Skip, hold Release) enforced in `workspace_controller.py`, not ACL
**Deprecated but still live** (cleanup is Phase 5):
- OWL components: `fp_shopfloor_tablet`, `fp_plant_overview` — registered but no menu points at them
- Endpoints: `/fp/shopfloor/tablet_overview`, `plant_overview`, `queue` — marked DEPRECATED with INFO log lines, bodies intact for back-compat
- `/fp/shopfloor/plant_overview/move_card` is **NOT** deprecated — the new Landing component uses it for drag-and-drop
**Old patterns to avoid:**
- Don't read `fp.job.name` for display — use `display_wo_name` everywhere on tablet/dashboard
- Don't add SCSS that uses raw hex without an `@if $o-webclient-color-scheme == dark` branch (dark mode breaks otherwise; see existing `_workflow_chip.scss` as the template)
- Don't add `web.assets_web_dark` entries to the manifest — Odoo 19 auto-compiles `web.assets_backend` SCSS into both bundles
- Don't bypass `_fp_should_block_predecessors()` when computing step blockers — keep `blocker_kind=predecessor` logic in sync with `can_start`
## Deployment ## Deployment

File diff suppressed because it is too large Load Diff

View File

@@ -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 `<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
```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.

View File

@@ -44,8 +44,6 @@
<field name="code">model._cron_autopause_stale_steps()</field> <field name="code">model._cron_autopause_stale_steps()</field>
<field name="interval_number">30</field> <field name="interval_number">30</field>
<field name="interval_type">minutes</field> <field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="active" eval="True"/> <field name="active" eval="True"/>
</record> </record>
</odoo> </odoo>

View File

@@ -1200,6 +1200,33 @@ class FpJobStep(models.Model):
# quick-look modal. The modal is bound via context= on the parent # quick-look modal. The modal is bound via context= on the parent
# job form's <field name="step_ids"/> — no TransientModel needed. # job form's <field name="step_ids"/> — no TransientModel needed.
# Job-level context for the quick-look modal — restored after commit
# b0070afc accidentally removed these while still referencing them in
# fp_job_step_quick_look_views.xml (entech caught the mismatch during
# the 2026-05-22 Phase 1-4 deploy).
quick_look_partner_id = fields.Many2one(
'res.partner',
string='Customer',
related='job_id.partner_id',
readonly=True,
)
quick_look_part_catalog_id = fields.Many2one(
'fp.part.catalog',
string='Part',
related='job_id.part_catalog_id',
readonly=True,
)
quick_look_qty = fields.Float(
string='Order Qty',
related='job_id.qty',
readonly=True,
)
quick_look_instruction_attachment_ids = fields.Many2many(
'ir.attachment',
string='Instruction Images',
related='recipe_node_id.instruction_attachment_ids',
readonly=True,
)
quick_look_instructions = fields.Html( quick_look_instructions = fields.Html(
string='Operator Instructions', string='Operator Instructions',
related='recipe_node_id.description', related='recipe_node_id.description',

View File

@@ -9,20 +9,26 @@
site that needs to bring legacy menus back can simply add a site that needs to bring legacy menus back can simply add a
user to the group. --> user to the group. -->
<!-- Reset group_ids on the 3 shopfloor menus that used to be <!-- Reset group_ids on the shopfloor menus that used to be
hidden — they are now the canonical UIs and should be visible hidden — they are now the canonical UIs and should be visible
to all users (subject to the original groups= attribute on to all users (subject to the original groups= attribute on
each menuitem in fusion_plating_shopfloor/views/fp_menu.xml). --> each menuitem in fusion_plating_shopfloor/views/fp_menu.xml). -->
<record id="fusion_plating_shopfloor.menu_fp_shopfloor_manager" model="ir.ui.menu"> <record id="fusion_plating_shopfloor.menu_fp_shopfloor_manager" model="ir.ui.menu">
<field name="group_ids" eval="[(6, 0, [ref('fusion_plating.group_fusion_plating_manager')])]"/> <field name="group_ids" eval="[(6, 0, [ref('fusion_plating.group_fusion_plating_manager')])]"/>
</record> </record>
<record id="fusion_plating_shopfloor.menu_fp_shopfloor_plant_overview" model="ir.ui.menu">
<field name="group_ids" eval="[(6, 0, [])]"/>
</record>
<record id="fusion_plating_shopfloor.menu_fp_shopfloor_tablet" model="ir.ui.menu"> <record id="fusion_plating_shopfloor.menu_fp_shopfloor_tablet" model="ir.ui.menu">
<field name="group_ids" eval="[(6, 0, [])]"/> <field name="group_ids" eval="[(6, 0, [])]"/>
</record> </record>
<!-- Phase 3 tablet redesign (2026-05-22) — the standalone Plant
Overview menu is superseded by Workstation > All Plant toggle.
The fp_menu.xml record was removed but the database row
persists (Odoo doesn't auto-delete orphan records). Force-
delete here so the menu disappears from the Shop Floor tree.
The action record (action_fp_plant_overview) is kept and
retargeted to fp_shopfloor_landing for bookmark back-compat. -->
<delete model="ir.ui.menu" id="fusion_plating_shopfloor.menu_fp_shopfloor_plant_overview"/>
<!-- bridge_mrp Production Priorities reference removed post-Sub 11 <!-- bridge_mrp Production Priorities reference removed post-Sub 11
(the bridge module is uninstalled and its menu xmlid no longer (the bridge module is uninstalled and its menu xmlid no longer
resolves). fp.job has its own priority field on the header. --> resolves). fp.job has its own priority field on the header. -->

View File

@@ -21,7 +21,7 @@ import logging
from markupsafe import Markup from markupsafe import Markup
from odoo import http from odoo import fields, http
from odoo.addons.fusion_plating.models.fp_tz import fp_format from odoo.addons.fusion_plating.models.fp_tz import fp_format
from odoo.http import request from odoo.http import request