From ad7d43f7205f97f8161e3e2b3cbe90429f61af75 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Mon, 13 Jul 2026 12:28:58 +0000 Subject: [PATCH] =?UTF-8?q?Configurable=20inactivity=20timeout=20=E2=80=94?= =?UTF-8?q?=20disabled=20automatically=20in=20PWA/standalone=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/AuthGate.tsx | 15 +++--- frontend/src/pages/Settings.tsx | 72 +++++++++++++++++----------- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/frontend/src/components/AuthGate.tsx b/frontend/src/components/AuthGate.tsx index ebe16dd..0bf7702 100644 --- a/frontend/src/components/AuthGate.tsx +++ b/frontend/src/components/AuthGate.tsx @@ -1,10 +1,12 @@ import { useEffect, useRef, useState } from 'react' import type { User } from '../types' -const SHARED_TIMEOUT_MS = 10 * 60 * 1000 - -function isSharedDevice() { - return document.cookie.split(';').some(c => c.trim() === 'hnf_shared_device=1') +function getInactivityMs(): number | undefined { + if (window.matchMedia('(display-mode: standalone)').matches) return undefined + const c = document.cookie.split(';').map(s => s.trim()).find(s => s.startsWith('hnf_inactivity_mins=')) + if (!c) return undefined + const mins = parseInt(c.split('=')[1]) + return isNaN(mins) || mins <= 0 ? undefined : mins * 60 * 1000 } interface Props { @@ -41,7 +43,8 @@ export function AuthGate({ children }: Props) { }, []) useEffect(() => { - if (state !== 'authed' || !isSharedDevice()) return + const ms = getInactivityMs() + if (state !== 'authed' || !ms) return async function forceLogout() { await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {}) @@ -51,7 +54,7 @@ export function AuthGate({ children }: Props) { function reset() { if (timerRef.current) clearTimeout(timerRef.current) - timerRef.current = setTimeout(forceLogout, SHARED_TIMEOUT_MS) + timerRef.current = setTimeout(forceLogout, ms) } const events = ['mousemove', 'keydown', 'click', 'touchstart'] as const diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index ecd7fbf..17ad27b 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -5,14 +5,23 @@ import { can, fmtGBP, type User } from '../types' const DEFAULT_MACHINE_NAMES = ['Front Desk', 'Restaurant / Bar'] -function isSharedDevice() { - return document.cookie.split(';').some(c => c.trim() === 'hnf_shared_device=1') +const TIMEOUT_OPTIONS = [ + { label: 'Off', value: 0 }, + { label: '5 min', value: 5 }, + { label: '10 min', value: 10 }, + { label: '15 min', value: 15 }, + { label: '30 min', value: 30 }, +] + +function getInactivityMins(): number { + const c = document.cookie.split(';').map(s => s.trim()).find(s => s.startsWith('hnf_inactivity_mins=')) + return c ? parseInt(c.split('=')[1]) || 0 : 0 } -function setSharedDeviceCookie(shared: boolean) { - document.cookie = shared - ? 'hnf_shared_device=1; max-age=31536000; path=/; SameSite=Strict' - : 'hnf_shared_device=; max-age=0; path=/; SameSite=Strict' +function setInactivityMins(mins: number) { + document.cookie = mins > 0 + ? `hnf_inactivity_mins=${mins}; max-age=31536000; path=/; SameSite=Strict` + : 'hnf_inactivity_mins=; max-age=0; path=/; SameSite=Strict' } interface SettingsData { @@ -52,7 +61,8 @@ export function SettingsPage({ user }: { user: User }) { const [testing, setTesting] = useState(false) const [refreshing, setRefreshing] = useState(false) const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null) - const [isShared, setIsShared] = useState(isSharedDevice) + const [inactivityMins, setInactivityMinsState] = useState(getInactivityMins) + const isPwa = window.matchMedia('(display-mode: standalone)').matches function flash(text: string, ok = true) { setMsg({ text, ok }) @@ -344,27 +354,33 @@ export function SettingsPage({ user }: { user: User }) { )} -
-
-

This Device

-

- {isShared - ? 'Marked as a shared computer — sessions time out after 10 minutes of inactivity.' - : 'Mark this as a shared computer to automatically log out after 10 minutes of inactivity.'} -

-
- { - const next = !isShared - setSharedDeviceCookie(next) - setIsShared(next) - flash(next ? 'This device is now marked as shared.' : 'Shared device mode removed.') - }} - > - {isShared ? 'Remove Shared Status' : 'Mark as Shared'} - +

This Device

+

+ {isPwa + ? 'Running as an installed app — inactivity timeout is disabled. Sessions last until the 30-day limit.' + : 'Set an inactivity timeout to automatically sign out after a period of no use. Use on shared or front-desk computers.'} +

+
+ {TIMEOUT_OPTIONS.map(opt => ( + + ))}