From 67fc22237ba9822cf4d26c13decfddd375988172 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 24 May 2026 14:36:12 -0400 Subject: [PATCH] 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) --- .../fusion_plating_shopfloor/__manifest__.py | 10 +- .../static/src/js/job_workspace.js | 4 +- .../static/src/js/manager_dashboard.js | 4 +- .../static/src/js/plant_kanban.js | 11 +- .../static/src/js/services/fp_rpc.js | 75 ++---------- .../static/src/js/services/tech_store.js | 42 ------- .../static/src/js/shopfloor_landing.js | 4 +- .../static/src/js/tablet_lock.js | 108 ++++++------------ .../static/src/xml/tablet_lock.xml | 14 +-- .../tests/test_tablet_pin.py | 57 --------- .../tests/test_tiles_bootstrap_fields.py | 18 +-- 11 files changed, 67 insertions(+), 280 deletions(-) delete mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/js/services/tech_store.js diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py index 56200b65..bfcb1a43 100644 --- a/fusion_plating/fusion_plating_shopfloor/__manifest__.py +++ b/fusion_plating/fusion_plating_shopfloor/__manifest__.py @@ -86,13 +86,11 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved. 'fusion_plating_shopfloor/static/src/scss/components/_kanban_card.scss', 'fusion_plating_shopfloor/static/src/xml/components/kanban_card.xml', 'fusion_plating_shopfloor/static/src/js/components/kanban_card.js', - # ---- Phase 6.2 tablet PIN gate ---- - 'fusion_plating_shopfloor/static/src/js/services/tech_store.js', + # ---- Tablet PIN gate ---- 'fusion_plating_shopfloor/static/src/js/services/activity_tracker.js', - # Phase D — tablet session manager (idle + ceiling timer). - # Used by tablet_lock when fp.shopfloor.tablet_session_mode - # ='session_swap' to call /fp/tablet/lock_session and reload - # the page so the browser re-bootstraps under the kiosk. + # Tablet session manager (idle + ceiling timer). Calls + # /fp/tablet/lock_session and reloads the page so the + # browser re-bootstraps under the kiosk. 'fusion_plating_shopfloor/static/src/js/services/tablet_session_manager.js', # Phase 6.3 — fpRpc wrapper. MUST load before any consumer # (job_workspace, shopfloor_landing, manager_dashboard, diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/job_workspace.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/job_workspace.js index b3d1d06f..8b34121b 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/js/job_workspace.js +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/job_workspace.js @@ -37,7 +37,7 @@ export class FpJobWorkspace extends Component { this.notification = useService("notification"); this.action = useService("action"); this.dialog = useService("dialog"); - this.techStore = useService("fp_shopfloor_tech_store"); + this.tabletSessionManager = useService("fp_tablet_session_manager"); this.state = useState({ data: null, @@ -105,7 +105,7 @@ export class FpJobWorkspace extends Component { // ---- Hand-Off (Phase 6.2) --------------------------------------------- handOff() { - this.techStore.lock(); + this.tabletSessionManager.lockBack("manual"); } onJumpToBlocker({ model, id }) { diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/manager_dashboard.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/manager_dashboard.js index 3b443d94..c34e4018 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/js/manager_dashboard.js +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/manager_dashboard.js @@ -28,7 +28,7 @@ export class ManagerDashboard extends Component { setup() { this.notification = useService("notification"); this.action = useService("action"); - this.techStore = useService("fp_shopfloor_tech_store"); + this.tabletSessionManager = useService("fp_tablet_session_manager"); this.state = useState({ overview: null, @@ -153,7 +153,7 @@ export class ManagerDashboard extends Component { // ---- Hand-Off (Phase 6.2) --------------------------------------------- handOff() { - this.techStore.lock(); + this.tabletSessionManager.lockBack("manual"); } toggleCard(jobId) { diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/plant_kanban.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/plant_kanban.js index d2994fc6..f13a4dd7 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/js/plant_kanban.js +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/plant_kanban.js @@ -40,12 +40,7 @@ export class FpPlantKanban extends Component { setup() { this.notification = useService("notification"); this.action = useService("action"); - // techStore may not be registered until first PIN unlock; guard with try. - try { - this.techStore = useService("fp_shopfloor_tech_store"); - } catch { - this.techStore = null; - } + this.tabletSessionManager = useService("fp_tablet_session_manager"); this.state = useState({ mode: "station", @@ -142,9 +137,7 @@ export class FpPlantKanban extends Component { } onHandOff() { - if (this.techStore && this.techStore.lock) { - this.techStore.lock(); - } + this.tabletSessionManager.lockBack("manual"); } onScanQr() { diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/services/fp_rpc.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/services/fp_rpc.js index 771fbbe2..9a6f061e 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/js/services/fp_rpc.js +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/services/fp_rpc.js @@ -1,74 +1,15 @@ /** @odoo-module **/ // ============================================================================= -// Fusion Plating — fpRpc() wrapper +// fpRpc — thin wrapper around @web/core/network/rpc // -// Drop-in replacement for the standard `rpc()` import. Automatically -// injects the current tablet_tech_id from the tech_store into every -// call, so server-side endpoints can attribute the action to the right -// user via env.with_user() (see env_for_tablet_tech in -// controllers/_tablet_audit.py). -// -// USE for any RPC that WRITES (start step, finish step, hold create, -// sign-off, milestone advance). For read-only loads (kanban, workspace -// load, manager funnel), plain rpc() is fine. -// -// Example: -// import { fpRpc } from "../services/fp_rpc"; -// await fpRpc("/fp/shopfloor/start_wo", { workorder_id: stepId }); -// -// Phase D — in `session_swap` mode, the tablet operates on a REAL Odoo -// session minted by /fp/tablet/unlock_session whose uid IS the tech. -// In that mode tablet_tech_id is redundant; the server attributes the -// write to request.env.user directly. We cache the mode at module -// level (refresh on every page load, exactly when the session may -// have flipped). +// Post-Phase-G of the tablet PIN session redesign: this no longer +// injects tablet_tech_id (the session uid IS the tech). Kept as a +// thin pass-through for backwards compatibility with callers that +// import fpRpc; a future cleanup could remove the wrapper entirely +// and update callers to use `rpc` directly. // ============================================================================= - -import { rpc as baseRpc } from "@web/core/network/rpc"; - -// Cached once per page load. Invalidated naturally by window.location.reload() -// after every lock/unlock (the JS bundle reinitializes, cache resets to null). -let _sessionModeCache = null; // 'legacy' | 'session_swap' | null (unknown) - -async function _getSessionMode() { - if (_sessionModeCache !== null) return _sessionModeCache; - try { - const res = await baseRpc("/web/dataset/call_kw", { - model: "ir.config_parameter", - method: "get_param", - args: ["fp.shopfloor.tablet_session_mode", "legacy"], - kwargs: {}, - }); - _sessionModeCache = res || "legacy"; - } catch (e) { - // If the lookup fails (network blip, ACL change), fail SAFE - // to legacy — that keeps tablet_tech_id injection on so the - // server-side audit attribution still works. - _sessionModeCache = "legacy"; - } - return _sessionModeCache; -} - -function _getTechStore() { - // Lazy-resolve via the global debug API — avoids circular service init - try { - const env = odoo.__WOWL_DEBUG__?.root?.env; - if (env && env.services && env.services.fp_shopfloor_tech_store) { - return env.services.fp_shopfloor_tech_store; - } - } catch (e) { - // ignore - } - return null; -} +import { rpc } from "@web/core/network/rpc"; export async function fpRpc(url, params = {}) { - const mode = await _getSessionMode(); - if (mode !== "session_swap") { - const techStore = _getTechStore(); - if (techStore && techStore.currentTechId) { - params = { ...params, tablet_tech_id: techStore.currentTechId }; - } - } - return baseRpc(url, params); + return rpc(url, params); } diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/services/tech_store.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/services/tech_store.js deleted file mode 100644 index 53519fc7..00000000 --- a/fusion_plating/fusion_plating_shopfloor/static/src/js/services/tech_store.js +++ /dev/null @@ -1,42 +0,0 @@ -/** @odoo-module **/ -// ============================================================================= -// Fusion Plating — Tech Store (shared OWL service) -// -// Holds the "current tech of record" for the locked tablet. Set by -// FpTabletLock on successful PIN unlock; cleared on auto-lock / Hand-Off. -// Other components read currentTechId via useService("fp_shopfloor_tech_store") -// and pass it through fpRpc() so server actions credit the right user. -// ============================================================================= - -import { reactive } from "@odoo/owl"; -import { registry } from "@web/core/registry"; - -export const fpShopfloorTechStore = { - start() { - const state = reactive({ - currentTechId: null, - currentTechName: "", - lockedAt: null, - }); - return { - get currentTechId() { return state.currentTechId; }, - get currentTechName() { return state.currentTechName; }, - get isLocked() { return !state.currentTechId; }, - setTech(id, name) { - state.currentTechId = id; - state.currentTechName = name; - state.lockedAt = null; - }, - lock() { - state.currentTechId = null; - state.currentTechName = ""; - state.lockedAt = Date.now(); - }, - state, // exposed for OWL reactive subscriptions - }; - }, -}; - -registry - .category("services") - .add("fp_shopfloor_tech_store", fpShopfloorTechStore); diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/shopfloor_landing.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/shopfloor_landing.js index 7dff654d..0b2d32f8 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/js/shopfloor_landing.js +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/shopfloor_landing.js @@ -38,7 +38,7 @@ export class FpShopfloorLanding extends Component { setup() { this.notification = useService("notification"); this.action = useService("action"); - this.techStore = useService("fp_shopfloor_tech_store"); + this.tabletSessionManager = useService("fp_tablet_session_manager"); this.state = useState({ mode: localStorage.getItem(LS_MODE) || "all_plant", @@ -126,7 +126,7 @@ export class FpShopfloorLanding extends Component { // ---- Hand-Off (Phase 6.2) --------------------------------------------- handOff() { // Tech walking away: lock the tablet so the next operator must PIN in - this.techStore.lock(); + this.tabletSessionManager.lockBack("manual"); } // ---- Search ------------------------------------------------------------ diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/tablet_lock.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/tablet_lock.js index 4ad1275b..57f635cb 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/js/tablet_lock.js +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/tablet_lock.js @@ -31,11 +31,11 @@ export class FpTabletLock extends Component { }; setup() { - this.techStore = useService("fp_shopfloor_tech_store"); this.activity = useService("fp_shopfloor_activity"); this.notification = useService("notification"); - // Phase D: idle + ceiling timer for session_swap mode. Started - // once tiles bootstrap shows we're already on a tech session. + // Post-Phase-G: session_swap is the only flow. The tablet + // session manager owns the actual idle-lock RPC; activity + // tracker drives the warning countdown UI. this.tabletSessionManager = useService("fp_tablet_session_manager"); this.state = useState({ @@ -49,8 +49,9 @@ export class FpTabletLock extends Component { clockText: this._formatTime(new Date()), dateText: this._formatDate(new Date()), company: null, - // Phase D — feature flag + kiosk identity from bootstrap - sessionMode: "legacy", // 'legacy' or 'session_swap' + // Kiosk identity from bootstrap so we can tell when the + // current browser session belongs to a tech (= unlocked) vs. + // the kiosk (= locked). kioskUid: null, currentUid: null, }); @@ -58,11 +59,13 @@ export class FpTabletLock extends Component { onMounted(async () => { await this._loadTiles(); this._tick = setInterval(() => this._checkIdle(), 1000); - // Heartbeat ping every 60s — for forensic visibility + // Heartbeat ping every 60s — for forensic visibility. Only + // ping while a tech is logged in; on the kiosk session this + // is just noise. this._ping = setInterval(() => { - if (this.techStore.currentTechId) { - rpc("/fp/tablet/ping", { current_tech_id: this.techStore.currentTechId }) - .catch(() => {}); + if (this.state.currentUid + && this.state.currentUid !== this.state.kioskUid) { + rpc("/fp/tablet/ping", {}).catch(() => {}); } }, 60000); // Clock tick — update visible HH:MM and date label every 60s. @@ -72,12 +75,11 @@ export class FpTabletLock extends Component { this.state.clockText = this._formatTime(now); this.state.dateText = this._formatDate(now); }, 60000); - // Session-swap mode: if we're already on a TECH session (uid - // != kiosk), start the idle/ceiling timer immediately. This - // handles the case where the page was reloaded after - // unlock_session minted the tech's session. - if (this.state.sessionMode === "session_swap" - && this.state.currentUid + // If we're already on a TECH session (uid != kiosk), start + // the idle/ceiling timer immediately. This handles the case + // where the page was reloaded after unlock_session minted + // the tech's session. + if (this.state.currentUid && this.state.currentUid !== this.state.kioskUid) { this.tabletSessionManager.beginSession(); } @@ -92,14 +94,10 @@ export class FpTabletLock extends Component { } get isLocked() { - // SESSION-SWAP MODE: the BROWSER session itself tells us whether - // a tech is unlocked — current_uid != kiosk_uid means unlocked. - // LEGACY MODE: defer to the techStore client-side flag. - if (this.state.sessionMode === "session_swap") { - return !this.state.currentUid - || this.state.currentUid === this.state.kioskUid; - } - return this.techStore.isLocked; + // The browser session itself tells us whether a tech is + // unlocked — current_uid != kiosk_uid means unlocked. + return !this.state.currentUid + || this.state.currentUid === this.state.kioskUid; } async _loadTiles() { @@ -109,9 +107,6 @@ export class FpTabletLock extends Component { const res = await rpc("/fp/tablet/tiles", { station_id: stationId }); if (res && res.ok) { this.state.company = res.company || null; - // Phase D — capture session_mode + kiosk/current uids so - // unlock() / isLocked / handOff can branch on mode. - this.state.sessionMode = res.tablet_session_mode || "legacy"; this.state.kioskUid = res.kiosk_uid || null; this.state.currentUid = res.current_uid || null; // Decorate each tile with an animation-delay (50ms staggered, @@ -130,14 +125,12 @@ export class FpTabletLock extends Component { } _checkIdle() { - // In session_swap mode, the tablet_session_manager owns the idle - // timer (it polls every 5s and calls /fp/tablet/lock_session - // directly). Skip this legacy 1s-poll path to avoid two parallel - // idle systems competing on the same tech session. - if (this.state.sessionMode === "session_swap") { - return; - } - if (!this.techStore.currentTechId) { + // Activity tracker drives the warning countdown UI; the actual + // idle-lock RPC is owned by tablet_session_manager (it polls + // every 5s and calls /fp/tablet/lock_session directly). This + // path just updates the visible "auto-lock in N seconds" banner. + if (!this.state.currentUid + || this.state.currentUid === this.state.kioskUid) { this.state.idleSecondsRemaining = null; return; } @@ -163,36 +156,19 @@ export class FpTabletLock extends Component { async unlock(pin) { try { - // SESSION-SWAP MODE: call the new endpoint, then reload the - // page so the browser re-bootstraps under the tech's session. - if (this.state.sessionMode === "session_swap") { - const res = await rpc("/fp/tablet/unlock_session", { - user_id: this.state.selectedTileUserId, - pin, - }); - if (res && res.ok) { - // Cookie has swapped. Reload so OWL/services re-init - // under the new (tech) session. The session manager - // (Task D1) picks up on the next page load. - // Match the legacy path's cleanup before reload kicks in. - this.state.selectedTileUserId = null; - window.location.reload(); - // Return a pending state so the caller doesn't try to - // navigate while we're tearing down. - return { ok: true, reloading: true }; - } - return { ok: false, error: (res && res.error) || "Unlock failed" }; - } - // LEGACY MODE: existing /fp/tablet/unlock path - const res = await rpc("/fp/tablet/unlock", { + const res = await rpc("/fp/tablet/unlock_session", { user_id: this.state.selectedTileUserId, pin, }); if (res && res.ok) { - this.techStore.setTech(res.current_tech_id, res.current_tech_name); - this.activity.bump(); + // Cookie has swapped. Reload so OWL/services re-init + // under the new (tech) session. The session manager + // picks up on the next page load. this.state.selectedTileUserId = null; - return { ok: true }; + window.location.reload(); + // Return a pending state so the caller doesn't try to + // navigate while we're tearing down. + return { ok: true, reloading: true }; } return { ok: false, error: (res && res.error) || "Unlock failed" }; } catch (err) { @@ -205,17 +181,9 @@ export class FpTabletLock extends Component { } handOff() { - // SESSION-SWAP MODE: the server destroys the tech session, then - // we reload to re-bootstrap as the kiosk. - if (this.state.sessionMode === "session_swap") { - this.tabletSessionManager.lockBack("manual"); - return; - } - // LEGACY MODE: client-side state flip only. - this.techStore.lock(); - this.state.selectedTileUserId = null; - this.state.idleSecondsRemaining = null; - this._loadTiles(); + // Server destroys the tech session, then we reload to + // re-bootstrap as the kiosk. + this.tabletSessionManager.lockBack("manual"); } // === 2026-05-24 redesign helpers ===================================== diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/xml/tablet_lock.xml b/fusion_plating/fusion_plating_shopfloor/static/src/xml/tablet_lock.xml index 12d3c549..075648e1 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/xml/tablet_lock.xml +++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/tablet_lock.xml @@ -75,16 +75,10 @@ - -