Commit Graph

171 Commits

Author SHA1 Message Date
gsinghpal
80887d6098 changes 2026-05-25 08:17:29 -04:00
gsinghpal
5d5964a327 fix(plant-kanban): full-height bordered columns + viewport-pinned scrollbar
Two layout polish fixes after persona-walk feedback on the new Plant
Kanban surface (`fp_plant_kanban`).

1. Columns now run full board height with visible borders
   Was: `.col` had `background: $plant-bg` (= page bg, invisible) and
   no border, so only the header card (`.o_fp_col_header`) drew any
   outline. Empty columns (BAKING / DE-RACKING / SHIPPING) looked
   unbounded — operators couldn't tell where one column ended and
   another began.

   Now: `.col` is the bordered white card (Trello / Asana style),
   stretches full height via grid + flex. `.o_fp_col_header` drops
   its standalone border / radius / background and is just a bottom-
   divider band inside the column card.

2. Horizontal scrollbar pinned to viewport bottom
   Was: `.o_fp_plant_kanban` was `min-height: 100vh` (block flow) +
   `.board` had `min-height: 520px; overflow-x: auto`. Scrollbar
   showed at the bottom of the .board element (~520px from top of
   board), floating mid-page below the empty columns.

   Now: parent is `height: 100vh; display: flex; flex-direction:
   column`. Header is `flex: 0 0 auto`; `.board` is `flex: 1 1 auto;
   min-height: 0` so it fills all remaining vertical and its
   scrollbar sits at the viewport bottom.

`.col-scroll` switched from `max-height: calc(100vh - 260px)` to
`flex: 1 1 auto; min-height: 0` so it expands inside the now-full-
height column instead of being capped at a magic number.

Version: fusion_plating_shopfloor 19.0.33.1.13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:40:41 -04:00
gsinghpal
80f80fb707 fix(tablet): ACL, loading hang, timer offset + FP-tz clock
Four fixes shipped together — all surfaced during tablet UX walkthrough
on entech.

1. sale.order ACL on step completion
   Technicians hit "Access Denied... sale.order" when starting/finishing
   any step. _fp_check_receiving_gate + the serial-promotion helpers +
   _fp_resolve_contract_review_part read step.job_id.sale_order_id (and
   sale_order_line_ids) without sudo. Per Rule 13m, denormalized cross-
   module reads in tablet controllers must sudo() the source recordset.

2. Workspace stuck on "Loading Job Workspace…" after Hand Off + relogin
   Action params aren't URL-encoded, so the workspace remounts with
   jobId=null. refresh() exited early, state.data stayed null, "Loading"
   shown indefinitely. onMounted now redirects to the plant kanban
   when jobId is null or the initial load returns no data.

3. 4-hour timer offset on active steps
   workspace_controller used fp_format() to serialize date_started —
   which converts naive UTC to user tz wall time first. JS then
   appended 'Z' and parsed as UTC, so timer was offset by the user's
   tz (4h on EDT). Switched to fp_isoformat_utc() (proper +00:00 ISO)
   and dropped the Z-append in formatActiveStepElapsed +
   isActiveStepOvertime.

4. Lock-screen clock follows FP regional setting
   tablet_lock.js used d.getHours() / d.toLocaleDateString() — browser
   tz. Now /fp/tablet/tiles returns tz_name (fp_user_tz resolution:
   user.tz → company.x_fc_default_tz → UTC) and the formatters use
   Intl.DateTimeFormat with the explicit timeZone option. plant_overview
   now consumes server_time (already fp_format'd) instead of toLocaleTime
   String. Same chain Odoo backend uses, so PDF / view / tablet all
   agree on what time it is.

Versions: fusion_plating_jobs 19.0.10.30.0,
fusion_plating_shopfloor 19.0.33.1.12.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:31:25 -04:00
gsinghpal
8d4c85cc52 fix(workspace): drop native confirm() on Close Receiving
Native browser confirm popups look out of place in the tablet UI.
Mark Counted is already a deliberate prior step, so requiring a
second confirmation on Close Receiving was just friction. If a
receiver hits Close prematurely, action_reset_to_counted on
fp.receiving from the back office is the recovery path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:36:48 -04:00
gsinghpal
fc17754996 fix(workspace): required-inputs gate fires + manager bypass dialog
Two bugs:

1. Gate silently passed when step.recipe_node_id was NULL — happened
   to every WO-30057 step after this morning's clone delete (the FK
   ON DELETE SET NULL wiped the link). _fp_missing_required_step_inputs
   returned an empty recordset when node was None, so the gate had
   nothing to fail on and button_finish succeeded with zero audit.
   Fix: _fp_check_step_inputs_complete now treats NULL recipe_node_id
   as an explicit "no recipe link" hard block. Operator can't finish;
   manager bypass posts chatter audit.

2. No tablet UI for the manager bypass. The gate's bypass was a
   Python context flag — invisible from the JS layer, so managers
   were stuck behind the same hard error as operators.
   Fix: new /fp/workspace/finish_step endpoint returns structured
   errors (gate type, missing_prompts list, bypass_available bool).
   Server-side enforces manager group when bypass=True (can't trust
   the client). New FpFinishBlockDialog OWL modal renders:
   - Non-manager: Cancel + Record Inputs
   - Manager:     Cancel + Record Inputs + ⚠ Bypass & Finish (audit)

JobWorkspace.onFinishStep routes plain finishes through the new
endpoint; signature-required steps still go through /fp/workspace/sign_off
(separate gate). Added is_manager to /fp/workspace/load payload so
the JS knows which dialog variant to render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:30:39 -04:00
gsinghpal
0371624afb feat(workspace): live HH:MM:SS timer on active step
Pure client-side tick — 1s setInterval bumps state.tickNow which the
template reads via formatActiveStepElapsed(step). No RPC per tick.
Reads step.date_started_iso (UTC) from the existing payload, parses
to ms, displays elapsed since.

- Green pill (#d1fae5 bg, monospace tabular-nums) on the ACTIVE badge
- Flips red (#fee2e2 + pulse animation) when elapsed > 1.5x
  duration_expected — visual cue for the operator that the step is
  running long against the recipe target

Cleanup interval on onWillUnmount alongside the existing 15s refresh
interval.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:18:49 -04:00
gsinghpal
eed1c4619d feat(workspace): pre-recipe receiving card with box count + damage log
Adds the receiver workflow to the Job Workspace tablet view (was the
gap behind WO-30057 sitting in Receiving with no way to advance).
Receivers no longer need to go to the backend form.

Workspace card (renders above the step list when fp.receiving in
state draft/counted on the linked SO):
- Draft state: numeric box-count input + per-line received_qty /
  condition picker (good/damaged/mixed) + Damage Log panel + Mark
  Counted button. Autosaves on input blur.
- Counted state: read-only summary (boxes, parts, who/when) +
  Damage Log still editable + Close Receiving button.
- Closed: card disappears, recipe takes over.

New FpDamageDialog OWL modal:
- Severity pill picker (Cosmetic / Functional / Rejected) with
  color-coded active state
- Required description textarea
- Action Required pill picker (None / Notify / Return / As-Is)
- Photo capture: both "Take Photo" (input capture="environment"
  triggers tablet camera) AND "Upload" (file picker fallback).
  Multi-photo with preview grid + per-photo remove.

5 new endpoints on workspace_controller.py:
- receiving_save_lines (autosave box_count_in + per-line qty/cond)
- receiving_mark_counted (wraps action_mark_counted)
- receiving_close (wraps action_close)
- damage_create (creates fp.receiving.damage + attaches base64 photos)
- damage_delete (removes a damage row)

No model changes — wraps existing fp.receiving actions and damage
CRUD. C3 (outbound shipping carrier/label) is a separate spec.

Spec: in-conversation brainstorm (C1+C2) following the 2026-05-24
workspace step actions spec; no standalone doc since scope is small.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:08:30 -04:00
gsinghpal
170398ab6f feat(workspace): per-kind step action buttons in Job Workspace
Fix: in the Job Workspace tablet view, the Start button was buried
inside a parent t-if that required the step to already be in_progress
or blocked. So ready/paused steps showed no buttons at all -
operators couldn't advance the WO from this screen (the reason the
user couldn't complete anything on WO-30057).

Template restructure (job_workspace.xml):
- Always-visible line 1 (icon + step# + name + ACTIVE/PAUSED badge + meta)
- Non-terminal detail panel (chips + instructions + opt-out + GateViz)
  visible on every non-done step so operator reads ahead
- Action row dispatched per-kind via getStepActions() helper

Per-kind action dispatcher (job_workspace.js):
- in_progress -> Record Inputs, Pause, Finish (or Finish & Sign Off)
- paused      -> Resume, Record Inputs, Finish
- contract_review (ready) -> Open QA-005 Form
- gating (ready)          -> Mark Passed (1-click start+finish)
- requires_rack_assignment -> Start (Assign Rack) - opens FpRackPartsDialog
- else (ready)            -> Start

5 new handlers: onPauseStep / onResumeStep / onMarkPassed /
onOpenContractReview / onStartWithRack. Pause and Resume use ORM RPC
(button_pause/button_resume) since no HTTP endpoint exists.

New model method (fp.job.step.action_mark_gating_passed):
- 1-click pass for gating steps - does button_start + button_finish
  in one transaction, posts chatter "Gate X marked passed by Y"
- Raises UserError if called on a non-gating step (defensive)
- Bypasses S21 required-inputs gate (gating steps have no inputs)

Controller: workspace_controller.py adds requires_rack_assignment to
the step payload so the JS dispatcher can route correctly.

Spec: docs/superpowers/specs/2026-05-24-workspace-step-actions-design.md
Sub-B (Record Inputs tablet polish: inputmode/prefill/date pickers/
signature pad/camera) is brainstormed but deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 18:38:22 -04:00
gsinghpal
9a2975b154 feat(jobs+shopfloor): recipe cleanup migration + no_parts column fix
Migration 19.0.10.26.0/post-migrate.py runs in 5 phases:
1. Resequence recipe 3620 ENP-ALUM-BASIC ops to fix the duplicate-
   sequence bug (Contract Review=10, Incoming Inspection=20,
   Masking=30, Racking=40, then the rest). Also delete the empty
   duplicate ENP-Alum Line sub_process (id 4056).
2. Backfill kind on all kind=other nodes via the extended resolver
   from fusion_plating 19.0.21.3.0
3. Delete all per-part clone recipes (name contains em-dash)
4. Recompute fp.job.step.area_kind on all steps
5. Recompute fp.job.active_step_id + card_state on in-flight jobs

Plant kanban: no_parts cards now always land in the Receiving column
regardless of active_step area_kind. The receiver works Receiving;
that's where the card belongs when parts haven't arrived.

Spec: docs/superpowers/specs/2026-05-24-recipe-cleanup-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:59:33 -04:00
gsinghpal
b06d28e7f6 feat(jobs+shopfloor): live-step priority chain + done-job filter
Fix the Shop Floor plant kanban so cards land in the right column:
- fp.job._compute_active_step_id walks priority chain
  (in_progress > paused > ready > pending), not just in_progress
- fp.job._compute_card_state edge case respects job.state='done'
  (no more bogus 'contract_review' label on done jobs)
- fp.job.step._compute_area_kind reads kind.area_kind directly;
  legacy _STEP_KIND_TO_AREA dict removed (50+ lines deleted)
- /fp/landing/plant_kanban filters out done/cancelled jobs from
  the live board

Migration 19.0.10.25.0 backfills template metadata (codes,
descriptions, icons, kind_id) on 30 unfinished library templates
and repoints recipe nodes for 6 unambiguous name patterns
(Blasting -> blast, Ready For X -> gating, De-Masking -> demask,
Scheduling -> gating, Nickel Strip -> wet_process,
Pre-Meas/Check Sulfamate -> inspect).

Battle test bt_s24_between_steps.py covers between-step routing,
paused step lifecycle, and done-job board filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:06:53 -04:00
gsinghpal
c75d2bde5a fix(shopfloor): session.authenticate signature update for Odoo 19
Odoo 19's Session.authenticate(env, credential) takes an Environment as
the first arg, not a db-name string. Passing request.db triggered
TypeError: 'str' object is not callable on the internal
env(user=None, su=False) reset.

Fixes the "Odoo Server Error" dialog operators saw when trying to PIN
unlock from the tablet. Same fix applies to lock_session (which was
silently masked by its broad except Exception).

Bumps fusion_plating_shopfloor to 19.0.33.1.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:56:05 -04:00
gsinghpal
9e6b88f60e chore(shopfloor): bump to 19.0.33.1.1 for lock_session kiosk-xmlid fix
Pure code change (no DB schema), but bumping the patch version
keeps repo manifest aligned with the deployed state so the next
-u doesn't no-op due to version match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:27:30 -04:00
gsinghpal
dc6afdd021 fix(shopfloor): lock_session resolves kiosk login via xmlid
The kiosk_login in /fp/tablet/lock_session was hardcoded to the
data XML's original value ('fp_tablet_kiosk@enplating.local'). The
data record is noupdate='1', so admins can (and on entech, did)
rename the kiosk user on the form for memorability — the rename
persists through -u, but the hardcoded string in the controller
silently breaks the re-auth-as-kiosk path.

Fix: resolve the kiosk login dynamically via env.ref of the xmlid
'fusion_plating_shopfloor.user_fp_tablet_kiosk'. Robust against any
future rename. CLAUDE.md updated to make 'identify by xmlid, never
by login string' an explicit convention for the tablet flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:26:46 -04:00
gsinghpal
b869c31ba3 chore(shopfloor): bump to 19.0.33.1.0 after Phase G cleanup
Records the legacy-tablet-flow-removed state. Triggers -u so the
module's installed version reflects the post-cleanup code (the
ir_module_module row shows 19.0.33.1.0 after deploy).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:38:03 -04:00
gsinghpal
67fc22237b cleanup(shopfloor): session_swap is the only tablet flow
Frontend cleanup completing Phase G of the tablet PIN session
redesign:

- tablet_lock.js: removed sessionMode branching (no legacy path).
  unlock() always calls /fp/tablet/unlock_session + reloads.
  handOff() always calls tabletSessionManager.lockBack('manual').
  isLocked uses currentUid vs kioskUid exclusively. _checkIdle
  still drives the warning UI via activity_tracker; the actual
  lock RPC is owned by tablet_session_manager.

- fp_rpc.js: simplified to a thin async pass-through around @web/core
  network rpc. tech_store-based tablet_tech_id injection is gone
  (the session uid IS the tech).

- tech_store.js: DELETED (replaced by per-session backend attribution
  + tablet_session_manager for lock state). Removed from manifest.

- Wrapper components (shopfloor_landing, job_workspace,
  manager_dashboard, plant_kanban): swapped useService('fp_shopfloor_tech_store')
  for useService('fp_tablet_session_manager'); techStore.lock() ->
  tabletSessionManager.lockBack('manual'). plant_kanban's defensive
  try/catch on the tech_store lookup is no longer needed.

- tablet_lock.xml: Hand-Off button no longer gated on sessionMode;
  always rendered.

- Tests: removed legacy TestTabletUnlock class from test_tablet_pin.py
  (covered the deleted /fp/tablet/unlock route). Dropped session_mode
  assertion from test_tiles_bootstrap_fields.py (the return key is
  gone post-Phase-G). kiosk_uid + current_uid assertions retained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:36:12 -04:00
gsinghpal
d9f2983ea7 cleanup(shopfloor): remove legacy /fp/tablet/unlock + _tablet_audit helper
Session-swap is now the only flow. Legacy /fp/tablet/unlock endpoint
deleted. _tablet_audit.py (env_for_tablet_tech helper) deleted with
its last caller gone. /fp/tablet/ping no longer takes current_tech_id
(session uid IS the tech). /fp/tablet/tiles drops tablet_session_mode
return key (kiosk_uid + current_uid retained for OWL isLocked logic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:31:21 -04:00
gsinghpal
3120612e35 cleanup(shopfloor): strip tablet_tech_id from 17 endpoints
Session swap makes attribution automatic via request.env.user — the
tablet_tech_id plumbing is dead code after the kiosk + per-tech-session
architecture lands. Removed kwarg from 3 endpoints in
manager_controller, 11 in shopfloor_controller, 3 in
workspace_controller. _tablet_audit.env_for_tablet_tech import gone
from all 3 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:29:51 -04:00
gsinghpal
2a93ece4ba feat(shopfloor): per-user 7-day tablet event smart button
Owner-only smart button on res.users form. Click opens the audit log
filtered to that user (both user_id and attempted_user_id, so
failed unlock attempts against a tile show up too).

Compute is non-stored: search_count on the audit model per user on
demand. Sudo'd because the audit model has Owner-only ACL — the
compute fires for the form-viewing user (Owner) who would see the
results anyway via the menu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:34:45 -04:00
gsinghpal
b26fa13044 feat(shopfloor): audit log list+form views, Owner-only menu
Plating > Configuration > Tablet Audit Log. Read-only list with
decoration (green=unlock, red=failed, warning=ceiling/force,
muted=manual/idle). Form shows full forensic detail incl. ip/ua.
Owner-only via groups=fusion_plating.group_fp_owner on the menu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:34:00 -04:00
gsinghpal
7ff46af192 fix(shopfloor): Phase D review findings — defensive cleanups + bootstrap test
Important I1: tablet_session_manager.beginSession() now calls
_removeListeners() (and clears any pending _tickHandle) defensively
at start. Prevents DOM listener leak on dev hot-reload or any path
that re-bootstraps without a clean endSession() first.

Important I2: tablet_lock._checkIdle() early-returns in session_swap
mode. The tablet_session_manager owns idle tracking there (5s poll,
calls /fp/tablet/lock_session directly). Was previously dormant by
accident because session_swap never populates the legacy techStore;
explicit guard makes the decoupling intentional.

Minor M5: session_swap unlock success now resets selectedTileUserId
before window.location.reload(), matching the legacy path''s
cleanup pattern. Cosmetic before reload kicks in.

Minor M9: New test_tiles_bootstrap_fields with 3 HttpCase tests
asserting /fp/tablet/tiles returns tablet_session_mode, kiosk_uid,
and current_uid. The OWL lock screen branches on all three — a
contract regression would silently break session_swap.

Minor M10: Added inline comment near _sessionModeCache declaration
in fp_rpc.js explaining the page-reload-invalidates-cache lifecycle.

Deferred (for future polish, NOT in this commit):
- I3 (_getSessionMode ACL gap for tech users — functionally correct,
  just suboptimal; cache fallback to ''legacy'' kicks in)
- M6 (wrapper component Hand-Off buttons no-op in session_swap)
- M7 (hardcoded idle/ceiling thresholds — server-configurable later)
- M8 (timer divergence vs activity_tracker — unify later)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:30:29 -04:00
gsinghpal
6d4b6284ad feat(shopfloor): fpRpc skips tablet_tech_id injection in session_swap mode
When tablet_session_mode='session_swap', the server attributes every
write via request.env.user — there's no need to pass tablet_tech_id
in the RPC params. Caches the mode lookup at module level so we don't
round-trip on every RPC.

Legacy mode unchanged — fpRpc still injects tablet_tech_id from
techStore.currentTechId.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:18:33 -04:00
gsinghpal
d8456fb9a3 feat(shopfloor): tablet_lock branches on tablet_session_mode
When ir.config_parameter[fp.shopfloor.tablet_session_mode]='session_swap',
PIN submit calls /fp/tablet/unlock_session and reloads the page; the
new session manager service kicks in on next mount. handOff() calls
lockBack('manual') which destroys the tech session server-side and
re-auths as kiosk.

Legacy mode unchanged — same /fp/tablet/unlock + techStore flow.

The feature flag, kiosk_uid, and current_uid arrive via the existing
/fp/tablet/tiles bootstrap response (Task D0).

Adds a tablet_lock-owned Hand-Off button visible only in session_swap
mode (in legacy mode wrapper components own their own buttons that hit
techStore.lock(); session_swap renders our own button so the manual
hand-off goes through lockBack() + page reload).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:17:53 -04:00
gsinghpal
b41d9629e1 feat(shopfloor): tablet_session_manager OWL service
Tracks idle + ceiling timers for an unlocked tech session. Fires
/fp/tablet/lock_session when either trips, then reloads the page so
the browser re-bootstraps under the fresh kiosk session.

Defaults: 10min idle, 8hr ceiling, 5s tick interval. Listens for
click/touchstart/keydown/mousemove as activity signals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:15:28 -04:00
gsinghpal
10477a7c8f feat(shopfloor): /fp/tablet/tiles returns session_mode + kiosk_uid
OWL lock screen needs to know (a) the active session mode (legacy or
session_swap) to branch between endpoints, and (b) the kiosk uid to
determine 'is the current browser session the kiosk?' Both come from
the bootstrap response so no extra round-trips on every render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:14:41 -04:00
gsinghpal
8f6302b446 fix(shopfloor): Phase C review findings — lock_session closes unlock event + cron test
Important 1: lock_session now closes the original unlock event's
session_ended_at via the same parameterized-SQL bypass pattern used
by the force-lock cron. Without this, every Hand-Off click became
a duplicate force_lock event 8 hours later (cron saw the unlock still
open and re-processed).

Important 2: test_unlock_lock_session_endpoints setUp now
unconditionally overrides the kiosk password (was gated on
'if not get_param(...)' which broke on entech where the post-migrate
hook already generated a random password — tests failed against the
real value). HttpCase rolls back per test so no persistence.

Minor 4: _cron_force_lock_stale_sessions now routes the force_lock
create through write_event helper for consistency (single audit-write
path; helper captures acting_uid/ip/ua uniformly).

Minor 5: Hoisted local imports inside method bodies to top-of-file
in tablet_controller.py (AccessDenied, _tablet_session_audit) and
fp_tablet_session_event.py (timedelta, write_event).

Minor 6: New test_force_lock_cron.py with 3 tests: stale session
emits force_lock + closes original; recent session unaffected;
already-closed session not re-processed. Would have caught
Important 1 if it had existed during Phase C review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:08:30 -04:00
gsinghpal
87e924d1d8 test(shopfloor): HTTP tests for unlock_session + lock_session
5 tests covering correct/wrong PIN, audit event writes, manual/idle
lock reasons. Uses HttpCase to actually drive the JSONRPC endpoint
end-to-end with session cookie handling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:55:19 -04:00
gsinghpal
7fab01e5cb feat(shopfloor): force-lock cron for stale tablet sessions
Every 5 minutes, find active unlock events past 8-hour ceiling and
mark them force-locked. SQL bypass of the model's read-only ACL is
the only path that can update existing rows (no Python write() works
because the model override blocks even sudo writes without the
explicit fp_tablet_audit_admin_write context flag).

Ceiling configurable via ir.config_parameter[fp.tablet.session_ceiling_hours].

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:54:44 -04:00
gsinghpal
4911088dc1 feat(shopfloor): /fp/tablet/lock_session destroys tech session
Writes lock event (manual/idle/ceiling) with duration computed from
the open unlock event. Then logout + re-authenticate as kiosk via
the password stored in ir.config_parameter['fp.tablet.kiosk_password'].

Falls back to 'needs_kiosk_relogin' if the kiosk password is missing
(sysadmin must log in manually). Logs every event for forensic
review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:54:08 -04:00
gsinghpal
086ff512b6 feat(shopfloor): /fp/tablet/unlock_session mints real Odoo session
PIN verify -> request.session.authenticate(type=fp_tablet_pin) -> new
session sid, cookie swap, audit event written. Failed attempts also
written to audit log (failed_unlock, failure_reason=wrong_pin or
locked_out or no_pin_set or user_inactive).

OLD /fp/tablet/unlock stays alive during the 1-week overlap window
per spec Section 5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:53:36 -04:00
gsinghpal
96e33834bd feat(shopfloor): _tablet_session_audit helper for audit-log writes
Single source for sha256(session sid), ua trim, ip/acting_uid capture
from request. Used by unlock_session, lock_session, and force-lock cron.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:53:03 -04:00
gsinghpal
765b095035 fix(shopfloor): Phase B review findings — C1/I1/I2/I3/M1
C1: Add placeholder fp_tablet_cron.xml + fp_tablet_session_event_views.xml
so the module is installable now (real content lands in Phase C task C4
and Phase E task E1 respectively).

I1: test_tablet_pin_auth_manager now passes {} (not self.env) as the
env arg to _check_credentials — matches what request.session.authenticate
provides and what the base implementation expects.

I2: Auth manager role check now uses user_sudo.all_group_ids (transitive)
instead of group_ids (direct) per CLAUDE.md rules 13l + 23. Owner users
who hold Owner directly still match all 5 shop-branch xmlids via the
implication chain.

I3: fp.tablet.session.event gains Python-layer write() + unlink()
overrides that always raise AccessError unless the explicit
fp_tablet_audit_admin_write / fp_tablet_audit_admin_purge context flag
is set. Closes the gap between the model's append-only docstring and
its actual enforcement (ACL-only previously).

M1: Hoisted 'from odoo.exceptions import AccessDenied' to top-of-file
imports next to existing UserError import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:47:26 -04:00
gsinghpal
358b90516b test(shopfloor): fp_tablet_pin auth manager handles all cases
8 tests: correct/wrong/missing PIN, missing/unknown login, inactive
user, no shop-branch role, and pass-through of other credential types.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:30:56 -04:00
gsinghpal
dd0dc26232 feat(shopfloor): fp_tablet_pin custom auth manager
Validates PIN hash + shop-branch role membership when the credential
type is fp_tablet_pin. Goes through Odoo's standard _check_credentials
chain so future 2FA / IP-gate modules layer cleanly on top.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:30:24 -04:00
gsinghpal
1dea752a29 test(shopfloor): fp.tablet.session.event is append-only
Owner reads. Technician cannot read. Owner cannot write or unlink.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:29:52 -04:00
gsinghpal
9f3edd60ae feat(shopfloor): fp.tablet.session.event append-only audit log
Captures unlock / failed_unlock / manual_lock / idle_lock /
ceiling_lock / force_lock / admin_reset events with session hash,
ip, user-agent, duration, failure reason, acting uid.

Read-only ACL granted to Owner in Phase A; no write/unlink anywhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:29:22 -04:00
gsinghpal
0b92294586 fix(shopfloor-sec): narrow kiosk ir.config_parameter scope + doc accuracy
Code-review findings on Phase A (Tablet PIN Session Redesign):

I1: Security XML comment now honestly describes the kiosk as Internal
User + explicit reads, not 'near-zero ACL'. base.group_user is kept
(required for auth='user' HTTP routes to function) but the comment
no longer overstates how locked-down the kiosk is.

I2: New ir.rule scopes the kiosk's ir.config_parameter read to keys
matching 'fp.tablet.%' or 'fp.shopfloor.%'. Combined with the
existing model-level read ACL, kiosk can no longer enumerate
third-party secrets (e.g. fusion_tasks.vapid_private_key) or
arbitrary API keys stored in ICP.

I3: post-migrate docstring now advises sysadmins to unlink the
plaintext ICP password row after kiosk tablets are paired, to
minimise plaintext-in-backups risk. Rotation procedure documented.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:22:40 -04:00
gsinghpal
a52ef29a84 test(shopfloor): kiosk user ACL has near-zero access
7 tests covering allowed reads (res.users, ir.config_parameter)
and forbidden everything else (fp.job, sale.order, fp.certificate,
fp.part.catalog, res.users write).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:06:52 -04:00
gsinghpal
97deb93ee7 feat(shopfloor): post-migrate hook for kiosk password init
Generates a random kiosk password on first deploy, stores in
ir.config_parameter for sysadmin retrieval. Idempotent — re-runs
on subsequent -u leave the password alone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:06:22 -04:00
gsinghpal
b67186a25b feat(shopfloor): create fp_tablet_kiosk user
Kiosk holds the tablet session when no tech is PIN-unlocked.
Password is auto-generated by the post-migrate hook (Task A5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:06:00 -04:00
gsinghpal
258782e3c3 feat(shopfloor-sec): kiosk ACL — read res.users + ir.config_parameter
Owner gets read on fp.tablet.session.event (audit log).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:05:42 -04:00
gsinghpal
acc95d8ee0 feat(shopfloor-sec): group_fp_tablet_kiosk for tablet kiosk session
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:05:14 -04:00
gsinghpal
e9b82fbe9d chore(shopfloor): bump to 19.0.33.0.0 for tablet PIN session redesign
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:04:55 -04:00
gsinghpal
7966f8d505 fix(shopfloor): tablet tiles domain uses group_ids (Odoo 19 rename)
Same mistake as the original implementer wave — used the deprecated
groups_id field name on res.users in the search domain. Odoo 19 raises
ValueError: Invalid field res.users.groups_id. Should be group_ids.

CLAUDE.md rule 13l example also fixed so future-Claude doesn't copy
the bug from the documentation.

Module version: 19.0.32.0.12 -> 19.0.32.0.13

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:48:29 -04:00
gsinghpal
839a7f0abc fix(shopfloor): tablet tile grid includes shop-branch role holders
Previously only direct Technicians appeared on the lock-screen tile
grid because env.ref('group_fp_technician').user_ids returns DIRECT
members only — Odoo's implication chain (Owner -> ... -> Technician)
is read-time only, not stored in res_groups_users_rel.

Search res.users with ('groups_id', 'in', shop_branch_ids) where
shop_branch_ids covers all 5 shop-branch role groups (Technician,
Shop Manager v2, Manager, Quality Manager, Owner). Sales branch
intentionally excluded — they don't operate the tablet.

Verified on entech: 18 technicians + 1 shop_manager + 2 managers
+ 1 quality_manager + 2 owners = 24 tiles (was 18).

CLAUDE.md rule 13l corrected — previous version wrongly claimed
res.groups.user_ids surfaced implied members. Now documents the
search-based query as the canonical 'enumerate role X or higher'
pattern.

Module version: 19.0.32.0.11 -> 19.0.32.0.12

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:47:01 -04:00
gsinghpal
0f751d82cc feat(shopfloor): add Record Inputs button to Job Workspace step row
Operators trying to Finish a step with required step_input prompts
got the S21 gate error telling them to 'Click Record Inputs on the
step row' — but the workspace UI never exposed that button. Only the
job-form view had it.

Adds a 'Record Inputs' secondary button next to Finish/Finish & Sign
Off when the step is active. Click opens the fp_record_inputs_dialog
(via action_open_input_wizard on fp.job.step). On dialog close the
workspace refreshes so the step's progress chip updates.

Module version: 19.0.32.0.10 -> 19.0.32.0.11

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:34:09 -04:00
gsinghpal
aa8161f764 fix(shopfloor): sudo job recordset in /fp/workspace/load (rule 13m)
Same pattern as plant_kanban — workspace payload denormalizes
cross-module fields Technician can't read directly (sale.order,
fp.part.catalog, customer_spec, etc.). job.sudo() at the top so
the whole render path is sudo'd.

Job Workspace was stuck on 'Loading...' with a server-error toast
because the route returned {ok:false, error:'...'} (27-byte response)
when the first cross-module field access AccessError'd.

Module version: 19.0.32.0.9 -> 19.0.32.0.10

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:28:58 -04:00
gsinghpal
31740b3949 fix(shopfloor): sudo cross-module reads in Plant Kanban _render_card
Post-migration, Technicians (now group_fp_technician) have read on
fp.job but NOT on sale.order / fp.part.catalog / fusion.plating.customer.spec.
The kanban render path tries to access job.sale_order_id.x_fc_po_number
and AccessErrors silently — kanban returns empty, user sees blank
'Shop Floor' page.

Fix: `job = job.sudo()` at the top of _render_card. The output is
denormalized display data, no security concerns; ACL gating is still
enforced by the caller's access to fp.job (which Technician does have).

CLAUDE.md rule 13m documents the broader pattern: any dashboard /
tablet / kanban controller surfacing cross-module data to low-priv
roles needs this sudo at the helper top.

Module version: 19.0.32.0.8 -> 19.0.32.0.9

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:19:39 -04:00
gsinghpal
e99cf20887 style(shopfloor): tablet lock clock 24h -> 12h with AM/PM
Operators read phone-style clocks; 24-hour was off-norm for North
American shop. Hour no longer zero-padded (1:05 PM, not 01:05 PM)
to match the iPhone/Android idiom.

Module version: 19.0.32.0.7 -> 19.0.32.0.8

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:16:17 -04:00
gsinghpal
cc5542833f style(shopfloor): tablet lock screen tile grid 3 -> 5 columns
Wider tablets fit 5 tiles per row comfortably; 3 was too sparse with
a 20-person operator roster (forced a long vertical scroll). Bumped
.o_fp_lock_tiles max-width from 480px to 800px so the tiles don't
stretch wide at 5 columns.

Module version: 19.0.32.0.6 -> 19.0.32.0.7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:13:52 -04:00
gsinghpal
42036c23ab fix(plating-perms): Phase I post-deploy fixes (live entech test catches)
8 distinct bugs caught + fixed while testing the live admin DB on entech
after the migration was approved. Each surfaced a real Odoo 19 gotcha
now codified in CLAUDE.md (rules 13b-13l).

Picker architecture:
- res.users.x_fc_plating_landing_action_id and res.company.x_fc_default_landing_action_id
  now Many2one('ir.actions.actions') instead of ('ir.actions.act_window'),
  so the picker accepts BOTH window actions (Sale Orders / Quotations /
  Process Recipes) AND client actions (Manager Desk / Plant Kanban /
  Quality Dashboard). Picker went from 3 entries to 6.
- x_fc_pickable_landing field moved from the two subclasses to the
  ir.actions.actions base. Single source of truth.
- _render_resolved on the base dispatches to the correct subclass by
  action type.

Non-admin Preferences access:
- Added ACL grant: group_fp_technician (and all higher roles via
  implication) get read on ir.actions.actions. Without this, opening
  Preferences raised AccessError on the picker domain evaluation.
- Removed the accessible_landing_action_ids Many2many compute (failed
  for non-admins because field assignment requires write access on
  the comodel relation, even with sudo'd search). Picker now shows all
  6 entries to all users; resolver falls through gracefully if the
  user picks an action they can't reach.
- res.users SELF_WRITEABLE_FIELDS / SELF_READABLE_FIELDS extended via
  @property + super() (NOT class attribute — Odoo 19 changed the
  pattern). Non-admin users can now save the Preferences dialog with
  plating fields without hitting the standard write ACL.

Migration workflow:
- res.groups.users -> .user_ids (Odoo 19 rename; deprecated alias
  removed). Was crashing _fp_notify_owners and _cron_purge_expired.
- user.message_post -> user.partner_id.message_post (res.users uses
  _inherits delegation which doesn't expose mail.thread methods).
  Was crashing the Owner approval click.

Tablet lock screen:
- /fp/tablet/tiles points at group_fp_technician instead of the old
  group_fusion_plating_operator. Post-migration nobody holds the old
  group directly (only via implication), so res.groups.user_ids on
  the old xmlid returned empty — 'No operators configured' shown
  even with PIN set.
- PIN pad dots dark mode: empty dot now dark gray (#424245), filled
  dot now pure white. Previous version had both at light shades so
  user couldn't see PIN entry progress.
- Lock-screen logo frame dark mode: near-opaque white plate
  (rgba 0.95) so company logos designed for light backgrounds
  render correctly. Previous 0.08 alpha let the dark page bleed
  through.

Pre-deploy collision fix (already committed before deploy but
documented here for completeness):
- pre-migrate.py to rename old configurator's 'Shop Manager' group
  display name before new fp_security_v2.xml loads the new
  group_fp_shop_manager_v2 with the same display name (avoids
  res_groups_name_uniq violation).

Module versions bumped:
  fusion_plating: 19.0.21.1.0 -> 19.0.21.1.2
  fusion_plating_shopfloor: 19.0.32.0.4 -> 19.0.32.0.6

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:02:32 -04:00