From 63b6240c22f82a89fb71adcbfb0ae12be954c2fe Mon Sep 17 00:00:00 2001
From: jtricerolph
Date: Mon, 13 Jul 2026 12:48:06 +0000
Subject: [PATCH] Add This Device tab to portal admin settings
---
src/pages/AdminSettings.tsx | 78 +++++++++++++++++++++++++++++++++++--
1 file changed, 75 insertions(+), 3 deletions(-)
diff --git a/src/pages/AdminSettings.tsx b/src/pages/AdminSettings.tsx
index 34695d3..b73056f 100644
--- a/src/pages/AdminSettings.tsx
+++ b/src/pages/AdminSettings.tsx
@@ -47,7 +47,7 @@ interface AppRow {
}
export function AdminSettings({ user }: { user: User }) {
- const [tab, setTab] = useState<'integrations' | 'rooms' | 'apps'>('integrations')
+ const [tab, setTab] = useState<'integrations' | 'rooms' | 'apps' | 'device'>('integrations')
const [integrations, setIntegrations] = useState([])
const [loading, setLoading] = useState(true)
@@ -68,7 +68,7 @@ export function AdminSettings({ user }: { user: User }) {
- {(['integrations', 'rooms', 'apps'] as const).map(t => (
+ {(['integrations', 'rooms', 'apps', 'device'] as const).map(t => (
setTab(t)} style={{
background: 'none', border: 'none', padding: '0.5rem 1rem',
fontSize: '0.85rem', fontWeight: 600, cursor: 'pointer',
@@ -76,7 +76,7 @@ export function AdminSettings({ user }: { user: User }) {
borderBottom: tab === t ? '2px solid var(--navy)' : '2px solid transparent',
marginBottom: '-1px',
}}>
- {t === 'integrations' ? 'Integrations' : t === 'rooms' ? 'Rooms & Sites' : 'App Settings'}
+ {t === 'integrations' ? 'Integrations' : t === 'rooms' ? 'Rooms & Sites' : t === 'apps' ? 'App Settings' : 'This Device'}
))}
@@ -95,6 +95,7 @@ export function AdminSettings({ user }: { user: User }) {
{tab === 'rooms' && }
{tab === 'apps' && }
+ {tab === 'device' && }
)
@@ -554,6 +555,77 @@ function AppsTab() {
)
}
+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 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'
+}
+
+function DeviceTab() {
+ const [inactivityMins, setInactivityMinsState] = useState(getInactivityMins)
+ const [saved, setSaved] = useState(false)
+ const isPwa = window.matchMedia('(display-mode: standalone)').matches
+
+ return (
+
+
+ {isPwa
+ ? 'This device is running as an installed PWA — inactivity timeout is automatically disabled. Sessions last until the 30-day sign-in limit.'
+ : 'Set an inactivity timeout for this device. Users will be signed out automatically after the chosen period of no activity. Use on shared or front-desk computers. Ignored when running as an installed PWA.'}
+
+
+
+ Inactivity timeout
+
+
+
+ {TIMEOUT_OPTIONS.map(opt => (
+ {
+ setInactivityMins(opt.value)
+ setInactivityMinsState(opt.value)
+ setSaved(true)
+ setTimeout(() => setSaved(false), 2000)
+ }}
+ style={{
+ padding: '0.4rem 1rem', borderRadius: '6px', fontSize: '0.85rem', fontWeight: 600,
+ cursor: isPwa ? 'not-allowed' : 'pointer', opacity: isPwa ? 0.4 : 1,
+ border: `1px solid ${inactivityMins === opt.value ? 'var(--navy)' : 'var(--card-border)'}`,
+ background: inactivityMins === opt.value ? 'var(--navy)' : 'var(--body-bg)',
+ color: inactivityMins === opt.value ? '#fff' : 'var(--text-dark)',
+ transition: 'all 0.15s',
+ }}
+ >
+ {opt.label}
+
+ ))}
+
+
+ {saved && (
+
+ ✓ Saved — takes effect immediately
+
+ )}
+
+ )
+}
+
const inputStyle: React.CSSProperties = {
background: 'var(--body-bg)', border: '1px solid var(--card-border)',
borderRadius: '6px', padding: '0.45rem 0.75rem', fontSize: '0.85rem',