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