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>
This commit is contained in:
@@ -89,6 +89,11 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
|||||||
# ---- Phase 6.2 tablet PIN gate ----
|
# ---- Phase 6.2 tablet PIN gate ----
|
||||||
'fusion_plating_shopfloor/static/src/js/services/tech_store.js',
|
'fusion_plating_shopfloor/static/src/js/services/tech_store.js',
|
||||||
'fusion_plating_shopfloor/static/src/js/services/activity_tracker.js',
|
'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.
|
||||||
|
'fusion_plating_shopfloor/static/src/js/services/tablet_session_manager.js',
|
||||||
# Phase 6.3 — fpRpc wrapper. MUST load before any consumer
|
# Phase 6.3 — fpRpc wrapper. MUST load before any consumer
|
||||||
# (job_workspace, shopfloor_landing, manager_dashboard,
|
# (job_workspace, shopfloor_landing, manager_dashboard,
|
||||||
# hold_composer) so `import { fpRpc }` resolves.
|
# hold_composer) so `import { fpRpc }` resolves.
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
// =============================================================================
|
||||||
|
// Tablet Session Manager (Phase D of tablet PIN session redesign)
|
||||||
|
//
|
||||||
|
// OWL service that tracks idle time + hard ceiling for an unlocked tech
|
||||||
|
// session and fires /fp/tablet/lock_session when either threshold trips.
|
||||||
|
//
|
||||||
|
// Activity events (click / touchstart / keydown / mousemove) reset the idle
|
||||||
|
// timer. setInterval polls every 5 seconds.
|
||||||
|
//
|
||||||
|
// Spec: docs/superpowers/specs/2026-05-24-tablet-pin-session-redesign-design.md
|
||||||
|
// =============================================================================
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { rpc } from "@web/core/network/rpc";
|
||||||
|
|
||||||
|
const DEFAULT_IDLE_MS = 10 * 60 * 1000; // 10 minutes
|
||||||
|
const DEFAULT_CEILING_MS = 8 * 60 * 60 * 1000; // 8 hours
|
||||||
|
const TICK_MS = 5000; // check every 5 seconds
|
||||||
|
|
||||||
|
export const tabletSessionManager = {
|
||||||
|
dependencies: [],
|
||||||
|
|
||||||
|
start(env) {
|
||||||
|
const service = {
|
||||||
|
idleMs: DEFAULT_IDLE_MS,
|
||||||
|
ceilingMs: DEFAULT_CEILING_MS,
|
||||||
|
lastActivity: Date.now(),
|
||||||
|
sessionStartedAt: null,
|
||||||
|
_tickHandle: null,
|
||||||
|
_running: false,
|
||||||
|
_touchHandler: null,
|
||||||
|
|
||||||
|
beginSession(sessionStartedAtMs) {
|
||||||
|
this.sessionStartedAt = sessionStartedAtMs || Date.now();
|
||||||
|
this.lastActivity = Date.now();
|
||||||
|
this._installListeners();
|
||||||
|
this._tickHandle = setInterval(() => this._tick(), TICK_MS);
|
||||||
|
this._running = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
endSession() {
|
||||||
|
if (this._tickHandle) clearInterval(this._tickHandle);
|
||||||
|
this._tickHandle = null;
|
||||||
|
this._removeListeners();
|
||||||
|
this._running = false;
|
||||||
|
this.sessionStartedAt = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
_installListeners() {
|
||||||
|
this._touchHandler = () => { this.lastActivity = Date.now(); };
|
||||||
|
["click", "touchstart", "keydown", "mousemove"].forEach(ev =>
|
||||||
|
document.addEventListener(ev, this._touchHandler, { passive: true })
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_removeListeners() {
|
||||||
|
if (!this._touchHandler) return;
|
||||||
|
["click", "touchstart", "keydown", "mousemove"].forEach(ev =>
|
||||||
|
document.removeEventListener(ev, this._touchHandler)
|
||||||
|
);
|
||||||
|
this._touchHandler = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async _tick() {
|
||||||
|
if (!this._running) return;
|
||||||
|
const now = Date.now();
|
||||||
|
const idleFor = now - this.lastActivity;
|
||||||
|
const sessionAgeMs = this.sessionStartedAt ? now - this.sessionStartedAt : 0;
|
||||||
|
let reason = null;
|
||||||
|
if (sessionAgeMs > this.ceilingMs) {
|
||||||
|
reason = "ceiling";
|
||||||
|
} else if (idleFor > this.idleMs) {
|
||||||
|
reason = "idle";
|
||||||
|
}
|
||||||
|
if (!reason) return;
|
||||||
|
this.endSession(); // stop ticking before the RPC
|
||||||
|
await this.lockBack(reason);
|
||||||
|
},
|
||||||
|
|
||||||
|
async lockBack(reason) {
|
||||||
|
try {
|
||||||
|
await rpc("/fp/tablet/lock_session", { reason });
|
||||||
|
} catch (e) {
|
||||||
|
// Even if the RPC fails, force a reload to drop the
|
||||||
|
// current session state — the cron will clean up.
|
||||||
|
console.warn("lock_session RPC failed; reloading anyway", e);
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return service;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.category("services").add("fp_tablet_session_manager", tabletSessionManager);
|
||||||
Reference in New Issue
Block a user