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 ----
|
||||
'fusion_plating_shopfloor/static/src/js/services/tech_store.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
|
||||
# (job_workspace, shopfloor_landing, manager_dashboard,
|
||||
# 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