diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-15 02:16:39 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-15 02:21:01 +0200 |
| commit | 73ad8e4eb566fe682107fa7e50ef624591199e99 (patch) | |
| tree | ff0017d014d46d8835487c080dca55c6def7fd6b /packages/meshbay-hub/src/meshbay_hub/static/idle.js | |
| parent | bdefcd025604f2c3009fe5e0cc01213c2ba62a6a (diff) | |
| download | meshbay-73ad8e4eb566fe682107fa7e50ef624591199e99.tar.gz | |
feat(hub): session lifetime is an admin setting, and a browser signs out when idle
Browser idle sign-out (media playback counts as activity; not the desktop app),
refresh idle window and maximum session length, in hours. Sign-out now revokes
on the hub, and the profile has "sign out everywhere".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XuNrwLf5EFWCMHzfoEvnpm
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/idle.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/idle.js | 77 |
1 files changed, 77 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/idle.js b/packages/meshbay-hub/src/meshbay_hub/static/idle.js new file mode 100644 index 0000000..e24f758 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/idle.js @@ -0,0 +1,77 @@ +// Signing a browser out after a stretch with nobody at it. +// +// The hub cannot measure this. It hears a token renewal every few hours from +// any open tab, attended or not, and nothing at all while a film plays over +// WebRTC. So the page decides, and the hub only says how long +// (`browser_idle_hours` in /v1/hub/info). +// +// Activity is input, or any <video>/<audio> on the page that is playing: a film +// nobody touches for two hours is somebody watching it. The last-active time +// lives in localStorage so every tab of this browser shares it — one idle tab +// must not sign out the tab in use — and a browser closed without signing out +// is caught the moment it is opened again. +// +// Never started in the desktop application (app.js): that is its owner's own +// machine, and it signs back in with its device key. + +const LAST_ACTIVE_KEY = 'mb_last_active'; +const CHECK_MS = 60000; +// Input fires constantly; the timestamp only has to be minutes-accurate. +const WRITE_EVERY_MS = 30000; +const INPUT_EVENTS = ['pointerdown', 'keydown', 'wheel', 'touchstart']; + +let lastWrite = 0; + +function readLastActive() { + try { + const v = Number(localStorage.getItem(LAST_ACTIVE_KEY)); + return Number.isFinite(v) && v > 0 ? v : null; + } catch { + return null; + } +} + +/** Record activity now. `force` skips the write throttle — a sign-in uses it. */ +function markActive(force = false) { + const now = Date.now(); + if (!force && now - lastWrite < WRITE_EVERY_MS) return; + lastWrite = now; + try { localStorage.setItem(LAST_ACTIVE_KEY, String(now)); } catch { /* private mode */ } +} + +function mediaPlaying() { + return [...document.querySelectorAll('video, audio')] + .some((m) => !m.paused && !m.ended); +} + +/** + * Watch for `idleMs` without activity, then call `onIdle` once. + * Returns the function that stops watching. + */ +function startIdleWatch(idleMs, onIdle) { + let fired = false; + const onInput = () => markActive(); + const check = () => { + if (fired) return; + if (mediaPlaying()) { markActive(); return; } + const last = readLastActive(); + // No record at all is a browser that predates this, not an idle one. + if (last === null) { markActive(true); return; } + if (Date.now() - last > idleMs) { fired = true; onIdle(); } + }; + + INPUT_EVENTS.forEach((e) => + window.addEventListener(e, onInput, { capture: true, passive: true })); + document.addEventListener('visibilitychange', check); + const timer = setInterval(check, CHECK_MS); + check(); + + return () => { + INPUT_EVENTS.forEach((e) => + window.removeEventListener(e, onInput, { capture: true })); + document.removeEventListener('visibilitychange', check); + clearInterval(timer); + }; +} + +export { startIdleWatch, markActive, LAST_ACTIVE_KEY }; |