Commit Graph

149 Commits

Author SHA1 Message Date
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
gsinghpal
de3ec7d97a feat(plating-sec): SO confirm gate + fix _administrator typo + Python sweep
Phase G of permissions overhaul.

G2: sale.order.action_confirm now requires group_fp_sales_manager
(spec Section 2.B). Sales Reps can save drafts but cannot move SOs
to 'sale' state. UserError raised with clear message if attempted.

G3: Fixed audit-finding-11 typo bug in 2 files. The original code
checked has_group('fusion_plating.group_fusion_plating_administrator'),
an xmlid that has NEVER existed - so the gate always returned False
and only the Manager-side check actually fired. Fixed both:
  - fusion_plating_invoicing/models/res_partner.py:34
  - fusion_plating_configurator/wizard/fp_direct_order_wizard.py:467
Both now check has_group('fusion_plating.group_fp_manager') which
transitively includes Owner via implied_ids.

G4: Swept all Python has_group() calls to reference new group xmlids.
Backward-compat keeps old refs working today (Phase A's implied_ids),
but the sweep ensures correctness after the 30-day rollback window
deletes old groups. Replacements:
  group_fusion_plating_operator    -> group_fp_technician
  group_fusion_plating_supervisor  -> group_fp_shop_manager_v2
  group_fusion_plating_manager     -> group_fp_manager
  group_fusion_plating_admin       -> group_fp_owner
  group_fusion_plating_cgp_officer -> group_fp_quality_manager
  group_fusion_plating_cgp_designated_official -> group_fp_owner
  group_fp_estimator               -> group_fp_sales_rep
  group_fp_accounting              -> group_fp_manager
  group_fp_receiving               -> group_fp_shop_manager_v2
  group_fp_shop_manager (legacy)   -> group_fp_manager

G1: test_sales_manager_gate.py covers the new confirm gate (SR
blocked, SMg allowed, Manager allowed via diamond implication).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 02:11:35 -04:00
gsinghpal
830b29ce49 feat(plating-landing): role-based dispatch resolver + picklist expansion
Phase E of permissions overhaul. The landing resolver now dispatches
based on the user's highest role (per spec Section 3):

  Owner          -> Manager Desk
  Quality Mgr    -> Quality Dashboard
  Manager        -> Manager Desk
  Sales Manager  -> Sale Orders
  Shop Manager   -> Plant Kanban (v2 layout) or Workstation (legacy)
  Sales Rep      -> Quotations
  Technician     -> Plant Kanban / Workstation

User override (x_fc_plating_landing_action_id) still wins; company
default and hardcoded Sale Orders are fallbacks. Layout-flag-aware via
ir.config_parameter['fusion_plating_shopfloor.layout'] (v2 vs legacy).

x_fc_pickable_landing field added to BOTH ir.actions.act_window AND
ir.actions.client (Manager Desk / Plant Kanban / Quality Dashboard
are client actions). Resolver helper polymorphically calls
_render_resolved() on either model.

Tagged 3 of 4 new actions pickable: Manager Desk, Plant Kanban,
Quality Dashboard. (action_fp_shopfloor_landing doesn't exist as an
XML record — it's a JS component name only; legacy layout falls
through to company default gracefully via raise_if_not_found=False.)

Tightened picklist domain to filter by user accessibility (Technician
no longer sees Manager Desk in the dropdown). New compute field
res.users.accessible_landing_action_ids runs check_access_rights on
each pickable action.

Tests in fusion_plating/tests/test_landing_resolver.py.

CLAUDE.md updated with two durable rules:
  - x_fc_pickable_landing lives on BOTH act_window and actions.client
  - Role-based dispatch precedence and helper API

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:56:37 -04:00
gsinghpal
36cd4341a7 feat(plating-menu): Layer 1+2 — explicit groups on top-level menus + submenus
Phase D Tasks D1-D4 of permissions overhaul. Adds explicit groups=
attributes to:
- 9 top-level Plating menus (matrix per spec Section 2.E)
- Quality submenus: Audits, Customer Specs, AVL → QM-only
- Compliance hub child submenus (CGP, General, Safety, Aerospace,
  Nuclear) → QM-only
- Operations submenus: Maintenance, Move Log, Labor History → Shop
  Manager+; Replenishment Suggestions → Manager+

Replaces fragile inheritance + action-ACL-based visibility with
explicit per-menu gates. Now every role's menu tree is deterministic.

Also adds fusion_plating/tests/test_menu_visibility.py — per-role
matrix tests using ir.ui.menu.search_count with the test user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:35:11 -04:00
gsinghpal
8eb2c2de95 refactor(plating-sec): sweep all ACL CSVs to new role group xmlids
Phase B of permissions overhaul. Mechanical text replacement across
11 ir.model.access.csv files:
  - group_fusion_plating_operator    -> fusion_plating.group_fp_technician
  - group_fusion_plating_supervisor  -> fusion_plating.group_fp_shop_manager_v2
  - group_fusion_plating_manager     -> fusion_plating.group_fp_manager
  - group_fusion_plating_admin       -> fusion_plating.group_fp_owner
  - group_fp_estimator (configurator)-> fusion_plating.group_fp_sales_rep
  - group_fp_accounting              -> fusion_plating.group_fp_manager
  - group_fp_receiving               -> fusion_plating.group_fp_shop_manager_v2
  - group_fp_shop_manager (legacy)   -> fusion_plating.group_fp_manager
  - group_fusion_plating_cgp_officer -> fusion_plating.group_fp_quality_manager
  - group_fusion_plating_cgp_designated_official -> fusion_plating.group_fp_owner

Backward-compat: old group xmlids still resolve (Phase A's implied_ids
chains keep old ACLs working for users still holding old groups).
This sweep ensures future-state correctness: when old groups are deleted
after the 30-day rollback window, ACLs continue resolving via the new
group xmlids.

Also adds fusion_plating/tests/test_acl_migration.py with sample-based
per-role access checks. The 2 CAPA tests are expected to fail until
Phase C implements the Manager/QM quality split.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:14:02 -04:00
gsinghpal
d89546bec7 fix(shopfloor): Back button + logo frame shape
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>
2026-05-23 22:11:51 -04:00
gsinghpal
818dfa3882 fix(shopfloor): bigger logo frame on the tablet lock screen
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>
2026-05-23 22:06:17 -04:00
gsinghpal
772107d25b feat(shopfloor): tablet lock-screen redesign — frontend + manifest
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>
2026-05-23 21:56:32 -04:00
gsinghpal
c61371005a feat(shopfloor): extend /fp/tablet/tiles payload with company block
LS-T1 of the tablet lock-screen redesign.

Adds 3 module-level helpers in tablet_controller.py:
  _initials_from(name)       — first/last initials for letter-mark fallback
  _avatar_gradient_for(uid)  — deterministic per-user color (8 gradients)
  _lock_company_payload(env) — company name + tagline + logo URL block

Endpoint /fp/tablet/tiles now returns:
  {ok, company:{id,name,tagline,logo_url,has_logo,initials},
   tiles:[{user_id, name, initials, avatar_url, has_photo,
           avatar_gradient, is_clocked_in, has_pin}, ...]}

Tagline reuses res.company.report_header (the existing invoice-letterhead
field) — no new model field. Falls back to 'Shop Floor Terminal' when
empty.

10 tests pass (initials edge cases, gradient determinism, payload shape).
The 'tagline matches input string' assertion was intentionally NOT added
— see new CLAUDE.md Critical Rule 22 about Odoo 19 HTML field
auto-wrapping that makes such an equality test brittle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:52:17 -04:00
gsinghpal
fd2b2908f3 fix(shopfloor): plant-view card sizing — match the mockup proportions
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>
2026-05-23 21:20:01 -04:00
gsinghpal
eb1fd50add fix(shopfloor): legacy client actions redirect to fp_plant_kanban
The plant-view rollout left two legacy ir.actions.client data records
still claiming tag='fp_shopfloor_landing':
  - action_fp_plant_overview        (Plant Overview)
  - action_fp_shopfloor_tablet      (Shop Floor — Tablet Station)

The landing-action resolver dispatched the new view correctly when the
user clicked the Plating root menu, but bookmarks / breadcrumbs /
QR-scan landings / direct URLs still routed through these legacy
actions and loaded the per-step kanban (OWL component is still
registered for back-compat).

Flipping their tag to fp_plant_kanban means every entry point now
opens the new view. The legacy fp_shopfloor_landing OWL component
stays registered (no code removed) but no XMLID points at it
anymore — safe to delete in a future cleanup.

Also documented this as a durable convention in CLAUDE.md under
'Legacy-action redirect (general rule for OWL component swaps)'.

Verified on entech:
  - action 1129 (Shop Floor)     tag: fp_shopfloor_landing → fp_plant_kanban
  - action 1133 (Plant Overview) tag: fp_shopfloor_landing → fp_plant_kanban
  - 3 stale asset bundles cleared
  - Module re-upgraded clean, registry rebuilt in 15.7s

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:14:33 -04:00
gsinghpal
a60506a645 feat(shopfloor): Phase 5 — flip default to v2 plant view + docs
PV-Phase5 of the plant-view redesign. Final phase — flips the default
of x_fc_shopfloor_layout from 'legacy' to 'v2' and updates CLAUDE.md
with the new architecture rule.

Verified on entech:
  - HTTP 200 on /web/login
  - Shopfloor module loads cleanly with all 19 new frontend files
  - /fp/landing/plant_kanban returns the assembled payload with 9
    columns + denormalized cards
  - Card state distribution: 22 contract_review + 8 no_parts + 1 running
    (sample data only — dev system)
  - Asset bundle re-compiled (9 stale attachments cleared)
  - ir.config_parameter['fusion_plating_shopfloor.layout'] = 'v2' set

To switch back to legacy: Settings → Fusion Plating → Shop Floor
Layout, or UPDATE ir_config_parameter SET value='legacy' WHERE
key='fusion_plating_shopfloor.layout'.

CLAUDE.md gets a new ~80-line section documenting:
  - Why the redesign (per-step kanban produced duplicate cards)
  - 9-column layout + step-kind → area mapping (spec D3, D4, D5)
  - 13-state catalog + precedence dispatch in _compute_card_state
  - Backend single-endpoint payload shape (/fp/landing/plant_kanban)
  - Frontend OWL component tree + critical implementation gotchas
    (rule 20 OWL scope, rule 8 SCSS @import, dark-mode compile-time)
  - How to switch back to legacy

Closes the 20-task plan in
  docs/superpowers/plans/2026-05-23-shopfloor-plant-view-plan.md

Spec: docs/superpowers/specs/2026-05-23-shopfloor-plant-view-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:59:44 -04:00
gsinghpal
8b9b4d60ad feat(shopfloor): Phase 4 — plant-view kanban frontend (OWL + SCSS + XML)
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>
2026-05-23 20:57:55 -04:00
gsinghpal
a90eace4d0 feat(shopfloor): Phase 3 — plant_kanban endpoint + dispatch
PV-Phase3 of the plant-view redesign.

- /fp/landing/plant_kanban JSONRPC endpoint returns {kpis, columns,
  cards} in one payload. One card per fp.job; cards denormalized so
  the OWL component doesn't fan out RPCs. Server-side filter handling
  for All / Mine / Running / Blocked / Overdue / FAIR. Within-column
  sort by (overdue, _SORT_PRIORITY[card_state], due_date).
- fusion_plating_shopfloor.action_fp_plant_kanban client action
  registered alongside the existing fp_shopfloor_landing action.
- fp_landing_data.xml resolver extended to read the layout flag and
  dispatch to v2 when x_fc_shopfloor_layout='v2' (default still legacy).

Card payload (23 fields): WO, customer, PN+rev, qty, PO, recipe, spec,
tags, current step + work centre, state chip, mini_timeline, operator,
icons (signoff / bake / tracking / etc.), progress.

State-chip mapping per spec §6.1 — one map keyed by card_state with
running-time elapsed, idle-hours, and operator-name interpolation.

Verified live — card payload sample on WO-30036 (contract_review state)
produces all expected keys + 9-element mini_timeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:51:36 -04:00
gsinghpal
63d692b322 feat(plating): Phase 1 — plant-view kanban data model foundation
PV-T1: fp.work.centre.area_kind Selection (9 floor columns)
PV-T2: fp.job.step.area_kind compute + _STEP_KIND_TO_AREA fallback
       (covers all 30+ step kinds in the project library, plus the
       spec D4 rule that de_mask folds into de_racking)
PV-T3: fp.job.step.last_activity_at + write hook + message_post
       override + fp.job.step.move.create() hook + _fp_is_idle helper
PV-T4: res.users.paired_work_centre_ids M2M (single-station for MVP,
       forward-compatible for Phase 2 multi-station picker)
PV-T5: res.config.settings.x_fc_shopfloor_layout feature flag backed
       by ir.config_parameter for the landing-action resolver

Migrations:
  fusion_plating 19.0.21.0.0      — backfill area_kind from kind
  fusion_plating_jobs 19.0.10.24.0 — backfill last_activity_at

Deployed + verified on entech:
  - 9/9 fp.work.centre rows have area_kind set
  - 400/400 fp.job.step rows have area_kind + last_activity_at
  - paired_work_centre_ids M2M relation table created
  - All 271 modules loaded cleanly, registry rebuilt in 27s

Part of the 2026-05-23 Shop Floor plant-view kanban redesign.
Plan: docs/superpowers/plans/2026-05-23-shopfloor-plant-view-plan.md
Spec: docs/superpowers/specs/2026-05-23-shopfloor-plant-view-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:43:15 -04:00
gsinghpal
1a3ca8704e feat(plating): session 2026-05-23 deploys — F1/F7/S22/S23 + UI fixes
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>
2026-05-23 20:37:17 -04:00
gsinghpal
27e12dd544 chore(shopfloor): register fp_rpc.js asset + bump to 19.0.30.2.0 (P6.3.6)
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
Adds the Phase 6.3 fpRpc wrapper to the web.assets_backend bundle.
Placed before its consumers so the `import { fpRpc } from "./services/fp_rpc"`
calls in job_workspace, shopfloor_landing, manager_dashboard, and
hold_composer resolve.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 00:47:51 -04:00
gsinghpal
5f03080374 feat(shopfloor): switch action-path RPCs to fpRpc + wire plant_overview/move_card (P6.3.5)
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>
2026-05-23 00:47:20 -04:00
gsinghpal
efaf16dffb feat(shopfloor): propagate tablet_tech_id to shopfloor + manager action endpoints (P6.3.3 + P6.3.4)
10 endpoints in shopfloor_controller (log_chemistry, start_bake, end_bake,
start_wo, stop_wo, bump_qty_done, bump_qty_scrapped, log_thickness_reading,
quality_hold, mark_gate) and 3 in manager_controller (assign_worker,
assign_tank, take_over) now accept a `tablet_tech_id` kwarg. Each rebinds
env via env_for_tablet_tech() so writes carry the correct uid even when
the OS session belongs to the persistent tablet user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 00:43:44 -04:00
gsinghpal
e4000374ca feat(fusion_plating_shopfloor): wire tablet_tech_id into workspace endpoints (P6.3.2)
hold, sign_off, advance_milestone each accept tablet_tech_id and
rebind env via env_for_tablet_tech. Writes (Hold.create, button_finish,
action_advance_next_milestone) now carry the tech-of-record's uid.
load endpoint is read-only and untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 00:37:58 -04:00
gsinghpal
fee4219703 feat(fusion_plating_shopfloor): fpRpc wrapper + env_for_tablet_tech helper (P6.3.1)
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>
2026-05-23 00:37:02 -04:00
gsinghpal
6ca9a58a8c chore(fusion_plating_shopfloor): bump 19.0.30.1.0 for Phase 6.2 — lock screen
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
Frontend lock screen ships:
- tech_store + activity_tracker shared OWL services
- FpPinPad, FpIdleWarning, FpPinSetup components
- FpTabletLock outer wrapper
- Wired into Landing/Workspace/Manager + Hand-Off button in each header
- fp_tablet_pin_setup client action for Preferences self-service
2026-05-23 00:33:42 -04:00
gsinghpal
d86c120969 feat(fusion_plating_shopfloor): FpPinSetup client action for self-service PIN (P6.2.6)
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>
2026-05-23 00:33:28 -04:00