4 new state-machine modes on FpTabletLock, reusing the existing
FpPinPad 4-cell component:
- request_code : 'Send temp PIN' button screen (no-PIN tile OR
after 3-fail Forgot button)
- enter_temp_code : 4-cell pad for the emailed code
- set_new_pin : 4-cell pad — choose new PIN
- confirm_new_pin : 4-cell pad — confirm new PIN
Trigger paths (per D1 + D2):
- Tap no-PIN tile -> goes straight to request_code mode
(onTileClick dispatches via tile.has_pin)
- Wrong PIN 3 times -> 'Forgot? Reset PIN via email' button appears
below the pad (gated by state.failedAttempts >= 3)
Client-side failedAttempts counter (resets on tile re-select per D14).
Server-side x_fc_tablet_pin_failed_count keeps incrementing to the
existing 5-fail lockout per D13.
After Confirm New PIN succeeds, auto-login fires unlock_session with
the new PIN. If unlock_session fails for any reason, falls back to
'PIN set, tap your tile to log in.' status.
SCSS reuses $lock-* tokens from _tablet_lock_tokens.scss — light +
dark handled by the existing token system (no new tokens needed).
Hand-Off gold gradient repeated for the primary 'Send temporary PIN'
button to match the existing tablet visual language.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback after first deploy: 7 KPI tiles wrapped to second line
(grid was repeat(5, 1fr) but I had added 2 new ones), and the
controls felt cramped.
Layout fix:
- .kpi-strip grid: repeat(5, 1fr) → repeat(8, 1fr) so the row stays
one line and there's room for the new Awaiting QC tile.
Missing KPI added:
- Awaiting QC — fp.job.card_state='awaiting_qc' count. Operators
couldn't see when QC was blocking job close from the KPI strip
(only visible inside the column). Server-side count + filter
clause + matching filter chip.
Visual polish (all light + dark via existing token system):
- KPI tiles: padding 6→10px, value font 20→26px, label font 9→10px,
subtle 135deg linear-gradient bg per kind (urgent/warn/good/qc),
hover lifts the tile with translateY + shadow.
- Filter chips: padding 4/12→7/16px, font 11→13px, gradient bg,
active state has gradient blue + shadow.
- Search input: padding 5/10→9/14px, font 12→14px, focus ring.
- Toolbar buttons (Station/All Plant/Manager/Scan QR/Hand Off):
padding 5/10→8/14px, font 12→14px, gradients, hover lift.
Dark mode handled automatically — all gradients reference
$plant-* tokens which already have @if $o-webclient-color-scheme ==
dark global overrides in _plant_tokens.scss.
Version bump fusion_plating_shopfloor 19.0.34.0.0 → 19.0.34.1.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Controller (plant_kanban.py):
- Widen domain: state IN (confirmed, in_progress, awaiting_cert,
awaiting_ship). Done jobs still drop off.
- _resolve_card_area: state=awaiting_cert → 'inspection' column,
state=awaiting_ship → 'shipping' column. State drives column
regardless of recipe shape.
- _state_chip: 🏷️ Awaiting CoC (amber) + 📦 Ready to ship (green).
- _SORT_PRIORITY: awaiting_cert=3.5, awaiting_ship=8.5.
- KPI dict: awaiting_cert + awaiting_ship counts.
- Filter clauses for the two new chips.
Model (fp_job.py):
- _compute_card_state handles new states in BOTH branches: the
no-active-step early return (where awaiting_cert/ship cards
land — all steps terminal) AND the per-step branch (defensive).
- _compute_mini_timeline_json: awaiting_cert paints inspection
dot 'current'; awaiting_ship paints shipping dot 'current'.
All earlier dots show 'done'.
SCSS (_plant_tokens.scss + _plant_card.scss):
- New tokens for amber (cert) + green (ship), light + dark variants
via the existing $o-webclient-color-scheme compile-time branch.
- .state-awaiting_cert / .state-awaiting_ship modifier classes
match the existing border-left pattern.
XML (plant_kanban.xml):
- Two new KPI tiles + two new filter chips wired to the state
filter clauses.
Manifest: fusion_plating_shopfloor 19.0.33.2.0 → 19.0.34.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Two fixes from live testing of the 2026-05-24 redesign:
1. Job Workspace Back button routed to deprecated component.
onBack() hardcoded tag: 'fp_shopfloor_landing' so tapping a card on
the new plant kanban -> opening the workspace -> clicking Back
dropped the user into the OLD per-step kanban (the legacy OWL
component the data-record redirects don't reach because doAction
bypasses the data layer).
Fix: change the hardcoded tag to 'fp_plant_kanban'. Grep
confirmed it's the only such reference in JS.
2. Logo frame shape — wider, shorter, logo bigger.
140x140 square -> 280x110 rectangle. Better fit for horizontal
company logos (mark + name + tagline laid out left-to-right).
Uniform 18px padding on all sides so the image breathes evenly.
Image area is ~244x74 (vs old ~104x104), so a typical horizontal
logo renders ~50% wider. border-radius 28->22 for the flatter
rect; letter-mark placeholder font 52->44 to fit the shorter
frame.
Also augmented CLAUDE.md 'Legacy-action redirect' rule with a new
'grep JS for hardcoded doAction' clause — the XML-record redirect
trick only covers ir.actions.client data; OWL components with inline
this.action.doAction({tag: ...}) calls bypass the data layer entirely
and need a separate sweep.
Asset cache cleared (3 stale attachments).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback after live testing: the 84px logo frame felt too small
and the image inside used only a fraction of the frame area.
Bumped the frame to 140px (1.67x) — image scales with the container
via the existing max-width/max-height: 100% rule. Proportional
adjustments to padding (14→18), border-radius (20→28), margin-bottom
(12→16), and the letter-mark placeholder font (32→52).
SCSS-only change. Asset cache cleared (3 stale attachments).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LS-T2..T6 of the tablet lock-screen redesign (LS-T1 backend shipped
separately in c6137100).
Files:
- _tablet_lock_tokens.scss (new — design tokens, dark/light branches
via $o-webclient-color-scheme, registered
first in manifest per project rule 8)
- tablet_lock.scss (full rewrite — gradient bg, glassmorphic
tiles, 4 entrance keyframes, hover lift,
click press, clocked-in pulse,
prefers-reduced-motion gate)
- tablet_lock.xml (extended — logo + clock + prompt blocks
wrapping the existing tile loop; tile
inner shape updated for avatar gradient,
has_photo conditional, is_clocked_in
modifier)
- tablet_lock.js (extended — state.clockText / dateText /
company, setInterval(60s) clock tick,
_formatTime / _formatDate / tileStyle /
avatarClass helpers per project rule 20)
- __manifest__.py (19.0.31.0.0 -> 19.0.32.0.0, registered
new tokens SCSS BEFORE tablet_lock.scss)
Verified live on entech:
- Module upgrade clean, registry loaded in 15.5s
- 6 stale asset attachments cleared
- Helpers in tablet_controller.py emit company payload + initials +
gradients correctly (Garry Singh -> GS, EN Tech -> ET, uid=5 ->
pink gradient)
- res.company.logo present (has_logo: True)
- All animations gated by prefers-reduced-motion per spec §6
CLAUDE.md updated with new Critical Rule 22 about Odoo 19 HTML fields
auto-wrapping plain-string writes — caught during Task 1 testing when
the original 'tagline equality' test failed because res.company.report
_header is an HTML field that wraps writes with <p> tags.
Closes the 6-task plan in
docs/superpowers/plans/2026-05-24-tablet-lock-screen-redesign-plan.md
Spec: docs/superpowers/specs/2026-05-24-tablet-lock-screen-redesign-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback after live testing: cards were too cramped on the 9-column
board. Restoring the Variant C mockup proportions and letting the board
scroll horizontally on smaller viewports (user explicitly accepted
side-scrolling).
Changes:
- .board grid: repeat(9, 1fr) → repeat(9, minmax(300px, 1fr))
plus overflow-x: auto. Each column ~300px so the card has room to
breathe. ~6 columns visible on 1920px desktop, ~4 on 1366px tablet,
smooth horizontal scroll for the rest.
- .col-scroll: gap 4→8, max-height eased so cards aren't packed.
- .o_fp_plant_card: padding 8/10→12, gap 4→6, base font 11→12.
- card-wo: 13→16 (matches mockup header size).
- card-step: 12→14.
- chips: padding 1/6→2/8, font 10→11, radius 10→12.
- mini-timeline blocks: 8→16px tall (current step 11→22px), labels
8→9px. Matches the mockup's punchy timeline strip.
- progress bar: max-width 60→100, height 3→4.
- operator pill / icon-row: bumped to match card scale.
No backend changes. SCSS-only. Asset cache cleared (3 attachments).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PV-Phase4 of the plant-view redesign. 19 new files implementing the
6-component OWL tree plus design tokens.
Components (each = JS + XML + SCSS triple):
- FpMiniTimeline — 9-step bar consuming mini_timeline_json
- FpPlantCard — Variant C card; 13 state-* CSS classes; tap
opens fp_job_workspace
- FpColumnHeader — column label + count badge + 'You're here'
badge when paired
- FpKpiTile — clickable KPI button with urgent/warn/good
variants and active state
- FpFilterChip — toggleable chip
- FpPlantKanban — top-level orchestrator: 10s polling, mode
toggle, search + 6 filter chips, board with
9 fixed columns, localStorage filter persistence
SCSS:
- _plant_tokens.scss (loads first, exposes $plant-* vars to every
later file — required because Odoo 19 forbids @import in custom
SCSS, manifest order IS the concat order)
- Dark mode via $o-webclient-color-scheme compile-time branch
Manifest registers all assets in dependency order: tokens → component
SCSS → component XML → leaf JS → top-level JS. Mirrors the existing
project pattern.
Critical patterns honored:
- Project rule 20 (no String/Number/parseInt in OWL templates):
all coercion in JS, string literals in foreach arrays.
- No t-out without markup() (none in this batch — all card text is
pre-formatted by the controller).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consolidated commit of session work already deployed to entech and
verified via the deep audit + the persona walk:
S22 — Signoff gate (fp.job.step.requires_signoff was 100% unenforced,
42/42 done steps had NULL signoff_user_id). Three-piece fix:
_fp_autosign_if_required (captures finisher on button_finish),
_fp_check_signoff_complete (raises UserError if NULL after autosign),
action_signoff (explicit supervisor pre-sign). Bypass:
fp_skip_signoff_gate=True.
S23 — Transition-form gate (same dormant-field shape as S22, caught
preventively before recipe authors flipped requires_transition_form
on). Model helpers on fp.job.step.move + controller gate in
move_controller (parts commit) + pre-reject in rack commit.
F7 — Chatter standardization: _fp_create_qc_check_if_needed,
_fp_fire_notification, _fp_create_delivery silent failures now also
post to job chatter instead of only logging to file.
UI fixes:
- Critical Rule 20 documented + applied: OWL templates only expose
Math as a global. Calling String(d) inside t-on-click throws
'v2 is not a function'. Fixed pin_pad.xml (string array instead of
number array with String() coercion). Also swept parseInt/
parseFloat in recipe_tree_editor + simple_recipe_editor.
- Notes panel HTML escape fix: chatter messages off /fp/workspace/load
were rendered via t-out, escaping the HTML. Wrap with markup() in
job_workspace.js refresh() before assigning to state.
Versions:
fusion_plating 19.0.20.8.0 → 19.0.20.9.0
fusion_plating_jobs 19.0.10.20.0 → 19.0.10.23.0
fusion_plating_shopfloor 19.0.30.2.0 → 19.0.30.5.0
All deployed to entech (LXC 111) and verified live.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JobWorkspace, ShopfloorLanding, ManagerDashboard, and the embedded
FpHoldComposer now call fpRpc() for write-path endpoints (start/finish
step, hold create, sign-off, milestone advance, work-centre move,
assign-worker, assign-tank, manager takeover). fpRpc auto-injects
tablet_tech_id from the tech_store so the server can rebind env via
env_for_tablet_tech() and credit the right user.
Read-path RPCs (workspace/load, landing/kanban, manager/overview,
manager/funnel, manager/approval_inbox, manager/at_risk, shopfloor/scan)
stay as plain rpc() — no audit benefit, no need for the extra plumbing.
Also wires tablet_tech_id into /fp/shopfloor/plant_overview/move_card
which I missed in P6.3.3 — surfaced when grepping JS for write callers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client-side fpRpc() is a drop-in for rpc() that automatically injects
tablet_tech_id from the tech_store into every action call. Read-only
endpoints can keep using plain rpc().
Server-side env_for_tablet_tech(env, tablet_tech_id) returns an env
scoped via with_user() when the id is a valid active user; otherwise
returns the original env unchanged. Controllers call this at the top
of action methods so all subsequent writes carry the right uid.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Registers fp_tablet_pin_setup as an ir.actions.client tag. Triggered
from res.users preferences via action_open_tablet_pin_setup (added
to res_users.py in P6.1.1). Three-stage flow:
loading → check if user has existing PIN via search_count
old → enter current PIN (skipped if first-time)
new → choose new PIN
confirm → enter new PIN again
done → success toast + auto-close 1.5s later
Each stage reuses FpPinPad with a different onSubmit + title. On
mismatch / server error, resets to the first stage with a notification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three OWL client actions all wrap their root in <FpTabletLock>:
ShopfloorLanding wraps o_fp_landing
JobWorkspace wraps o_fp_ws
ManagerDashboard wraps o_fp_manager
Each adds FpTabletLock to static components, imports tech_store, and
gains a handOff() method that calls techStore.lock(). The Hand-Off
button (yellow, lock icon) lands next to the scan/QR controls in each
header — pressing it instantly returns the tablet to the tile grid
without waiting for the idle timer.
Component composition (per spec §6.5):
FpTabletLock
if isLocked → tile grid + FpPinPad
else → existing client action (via <t t-slot="default"/>) + FpIdleWarning
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Top-level wrapper that renders lock screen (tile grid + PIN pad) when
no tech is signed in, and renders <t t-slot="default"/> otherwise.
Drives the auto-lock countdown via the activity_tracker service +
sends a /fp/tablet/ping heartbeat every 60s while a tech is signed in.
Tiles fetch from /fp/tablet/tiles using the localStorage station id
(set by ShopfloorLanding on QR pair / station picker selection).
State machine for the lock screen body:
loadingTiles → tiles list → tile tapped → PinPad → unlock RPC
↑
onPinCancel → back to tiles
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixed-position yellow-border overlay + countdown toast shown during
the last N (default 30) seconds before auto-lock. Pure props-driven —
secondsRemaining is the only input; parent (FpTabletLock) decides
when to mount and unmount. Box-shadow pulse animation runs CSS-only
so OWL doesn't need to re-render every tick.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reusable 4-digit PIN pad. Auto-submits on the 4th digit via the
onSubmit prop. On wrong PIN, shake animation + dots clear + error
banner (caller controls the message via the returned {ok:false, error}).
Used by FpTabletLock (unlock flow) and FpPinSetup (set/change flow).
Dark-mode SCSS branch follows the same $o-webclient-color-scheme
pattern as the rest of the shopfloor components.
Also registers tech_store + activity_tracker services in the asset
bundle (assets/web.assets_backend) before the pin_pad files, since
the pin_pad/tablet_lock components consume them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two registry-level services:
tech_store Shared reactive state holding currentTechId after a
successful PIN unlock. Other components subscribe via
useService("fp_shopfloor_tech_store") and read
currentTechId to inject into action RPCs. setTech(id, name)
on unlock; lock() on auto-lock / Hand-Off.
activity_tracker Document-level event tracker for pointerdown / touchstart
/ keydown / visibilitychange. Mouse-move alone deliberately
EXCLUDED — a tool resting on a tablet would otherwise keep
the session alive indefinitely. Public API:
bump(), getSecondsUntilLock(), getWarnThresholdSec()
Reads thresholds from ir.config_parameter at start +
every 5 min (so manager edits propagate within a shift).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan tasks P4.5 through P4.10 batched. Existing 3-column Plant Board
becomes one tab of four; adds Workflow Funnel (default), Approval
Inbox, and At-Risk siblings. Adds 2 new KPI tiles for Pending Cert +
At-Risk.
WORKFLOW FUNNEL (default tab)
Calls /fp/manager/funnel. Renders one row per fp.job.workflow.state
with stage chip + count + top 5 WO cards. Tap a card → JobWorkspace.
Bar chart bar behind each row scales with stage count.
APPROVAL INBOX
Calls /fp/manager/approval_inbox. Three strips: Holds to Release,
Certs to Issue, Scrap to Review. Per-row open + Open Workspace
buttons. Tab badge shows total pending count.
PLANT BOARD (existing — relocated as one tab)
The 3-column Needs Worker / In Progress / Team layout that already
exists, wrapped in t-if="activeTab === 'plant_board'". No behaviour
change — still uses /fp/manager/overview with 8s refresh.
AT-RISK
Calls /fp/manager/at_risk. 3 sub-panels: Trending Late (sorted by
late_risk_ratio desc), Hold Reasons (read_group), Bottleneck heatmap
(bottleneck_score from P4.1 with red/yellow/green bars).
KPI STRIP (new conditional tiles)
Pending Cert — count from inbox.certs_to_issue, click to open Inbox tab.
At-Risk — count from at_risk.trending_late, click to open At-Risk.
Auto-refresh: 8s for /fp/manager/overview (existing); the active tab's
data also refreshes every 8s via refreshActiveTab().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan tasks P3.2 + P3.3 + P3.4 batched. Full ShopfloorLanding OWL
client action — replaces fp_shopfloor_tablet AND folds in
fp_plant_overview.
Header strip Title, station chip, station picker dropdown,
Station/All-Plant mode toggle, QR scan controls,
last-refresh indicator.
KPI strip 4 tech-relevant tiles: Ready · Running ·
Bakes Due (warning) · Holds (red when > 0).
Search Live debounced (200ms) across WO# + customer +
part. ESC clears.
Kanban board Columns = work centres from /fp/landing/kanban.
Cards = FpKanbanCard (Phase 1 — P1.7).
Drag-and-drop reuses existing
/fp/shopfloor/plant_overview/move_card.
Card tap doAction → fp_job_workspace with
{job_id, focus_step_id}.
QR scan FP-STATION pairs, FP-JOB / FP-STEP jump to the
Workspace.
Mode + station_id persist in localStorage (LS_STATION_ID, LS_MODE).
Auto-refresh every 15s; suppressed during a drop and for 5s after.
Registers client action `fp_shopfloor_landing`. Menu rewire + endpoint
stubs land in P3.5 + P3.6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P1.7. Final shared service — standard WO card used on Landing
kanban, Manager Plant Board, and Workflow Funnel. Embeds WorkflowChip,
shows progress bar, priority dot, blocker badge from step.blocker_kind.
Density prop ('compact' vs 'normal') swaps padding for funnel use.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P1.6. Modal hold-creation form: reason picker, qty split,
optional photo (camera input on mobile), description, mark-for-scrap
toggle. Calls /fp/workspace/hold (added in P1.9). Reason list kept
client-side, keep in sync with fusion.plating.quality.hold.hold_reason.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P1.5. Modal canvas signature capture using HTML pointer events
+ Odoo Dialog service. Returns image/png dataURI via onSubmit callback;
caller decides what to do with it (e.g. /fp/workspace/sign_off attaches
to fp.job.step).
Canvas stays light even in dark mode for signature legibility.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P1.4. "Can't start yet — Waiting on Step N: X" block reused
across JobWorkspace step rows and Manager Plant Board cards. Icon set
maps to blocker_kind (predecessor/contract_review/parts_not_received/
racking_required/manager_input). Optional Jump button propagates to
parent via onJump callback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P1.3. Bootstraps the tests/ dir and adds the first of 5
shared OWL services. Pill renders fp.job.workflow.state with color
mapping + optional next-action hint.
Per CLAUDE.md "Dark Mode" rule: registered once in web.assets_backend;
Odoo 19 auto-compiles into both bright and dark bundles via the
\$o-webclient-color-scheme SCSS branch.
Version bumped to 19.0.27.0.0 (Phase 1 — Workspace foundation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues on the Process Tree client action:
1. Back to Work Order kept growing breadcrumbs (WO -> Tree -> WO ->
Tree -> ...) because onBack used action.doAction() which PUSHES
a new act_window onto the stack instead of popping. Fixed by
trying action.restore() first (pops the Tree off the stack and
returns to the parent WO/Step controller). Falls through to
explicit doAction only when there's no parent in the stack
(direct URL access).
2. The empty-state banner referenced productionId, a dead variable
from the bridge_mrp era when the tree was tied to mrp.production.
Since the component now uses jobId (fp.job context key), the
"No manufacturing order selected" message ALWAYS fired regardless
of whether a job was loaded. Fixed by using jobId and updating
the message to "No work order selected".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two improvements to the Process Tree visualization opened from the
Work Order's Process Tree header button:
1. Back button returns to the Work Order (job form) instead of
Plant Overview. fp.job.action_open_process_tree now passes
back_job_id in the client-action context; process_tree.js
reads it via a new backJobId getter, updates the button label
to "Back to Work Order", and routes onBack to fp.job form.
The Plant Overview fallback stays for callers that don't pass
either back_step_id or back_job_id.
2. Completed operation/step cards now have a green fill (#1e8449)
and a subtle pulsing glow (box-shadow animation, 2.6s alternate)
so finished work pops against still-pending dark cards. Hover
pauses the animation so the click target is steady. Reuses the
same green the workflow-state slice already used.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gap 1 — Rack Travel Ticket PDF (Sub 12b's Save+Print 404):
+ report_fp_rack_travel.xml in fusion_plating_reports — A5 landscape
single page, big rack name, Code 128 of FP-RACK:<name>, tag chips,
contained part-batches table.
+ ir.actions.report bound to fusion.plating.rack so it appears in
the rack form's Print menu too.
+ Sub 12b's rack_parts_dialog.js Save+Print URL fixed to use the
standard /report/pdf/<xmlid>/<id> route.
Gap 2 — Per-customer cert statement:
+ res.company.x_fc_default_cert_statement (company-level fallback).
+ res.partner.x_fc_cert_statement (per-customer override).
+ Surfaced on the partner form under the existing Cert + Document
Routing block.
+ Chronological CoC body resolves: customer override → company
default → hardcoded AS9100/ISO 9001 boilerplate. Three-tier
fallback so existing certs without overrides keep working.
Gap 3 — Chronological CoC 'Actual' column:
+ Build a captured_values_by_input dict from the move's
transition_input_value_ids (Sub 12b captures these on every
Move Parts commit).
+ Render typed Actual: text → as-is, number → with target unit,
boolean → PASS/FAIL, date → formatted, attachment → '[Attachment]'
placeholder.
+ Falls back to prompts from the destination step's step_input list
when no values were captured (still useful as audit-of-what-was-
asked even if blank).
Version bumps:
fusion_plating → 19.0.10.3.0
fusion_plating_reports → 19.0.10.1.0
fusion_plating_certificates → 19.0.5.3.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Controller: extend /fp/shopfloor/plant_overview return payload to
include 'racks' array (filtered to loaded/in_use/awaiting_unrack
states). Each entry has tag chips, part count, current node
breadcrumb, current step + tank code, and a precomputed
next_step_id (next sequence in the job's recipe — operator
overrides at runtime in the Move Rack dialog).
JS: state.racks populated from payload. New openMoveRackDialog()
method spawns FpMoveRackDialog. Notification when rack has no
successor (last step of job).
XML: top section above the existing work-centre columns. Renders
rack rows with tags, part count, breadcrumb, and primary MOVE RACK
button per row. Visible only when state.racks.length > 0.
SCSS: minimal styling for the racks pane (extends move_dialogs.scss
to keep all Sub 12b styles in one file).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ShopfloorTablet component:
- Imports the 3 new OWL dialogs.
- useService('dialog') for spawning.
- Listens for 'fp-resolve-rack' window CustomEvent fired from inside
FpMovePartsDialog → spawns FpRackPartsDialog inline.
- New methods: openMovePartsDialog(from, to) + openStopTimerDialog(id).
Refresh tablet after commit/reconcile so the UI reflects new state.
Listener cleanup on unmount.
Note: the actual buttons that call these methods are added to the
existing tablet XML in a follow-up step — for now they are wired but
not surfaced. Operators get them after Task 16 + smoke test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3-column grid layout for field rows (label / input / hint).
Compliance prompts + Blockers blocks have their own backgrounds.
Soft blockers amber + left border, hard blockers red + left border —
matches the spec's protocol.
Token pattern + dark-mode @if branch (CLAUDE.md rule: Odoo 19 doesn't
flip dark mode via runtime DOM class; we branch at SCSS compile time
on $o-webclient-color-scheme).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>