Kiosk: live-force reset to welcome screen, configurable inactivity timeout
Kiosk's SSE connection now stays open for the whole session instead of closing the moment a booking is pushed, so a staff-triggered Reset (from the extension/Terminals popup or the Devices admin page) now actually forces the tablet back to the welcome screen immediately, discarding whatever was mid-entry — not just flipping its status in the DB with nothing listening tablet-side. Removed the two now-redundant/harmful pushIdle() calls in routes/checkin.js's own submit success/failure paths, which would otherwise race the confirmation/error screen now that the stream stays open. Added a confirm() prompt before Reset on both trigger surfaces since it's now a real interruption, not just an unstick. Also made the tablet's inactivity auto-cancel timeout configurable from Settings (checkin_inactivity_timeout_minutes, default 5, same as the old hardcoded value) instead of a fixed constant, read via the existing token-gated GET /api/checkin/:token response.
This commit is contained in:
parent
790431c446
commit
5180adf8d2
10 changed files with 84 additions and 14 deletions
|
|
@ -48,6 +48,12 @@ export default function Devices() {
|
|||
}
|
||||
|
||||
async function handleReset(deviceId: string) {
|
||||
// Reset now pushes a live 'idle' event straight to the tablet (2026-10-09,
|
||||
// see Kiosk.tsx) rather than only flipping its status in the DB — if a
|
||||
// guest is mid check-in on it, this sends them back to the welcome screen
|
||||
// immediately and discards whatever they'd filled in, not just unsticking
|
||||
// a tablet with no one at it.
|
||||
if (!confirm('Reset this tablet? If a guest is mid check-in, this returns them to the welcome screen immediately and loses anything they’ve entered.')) return
|
||||
try {
|
||||
await resetDevice(deviceId)
|
||||
setMsg('Device reset to idle')
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export default function Settings() {
|
|||
const [settings, setSettings] = useState<AppSettings>({
|
||||
regcard_custom_field_id: '',
|
||||
online_checkin_complete_custom_field_id: '', terms_and_conditions_text: '',
|
||||
excluded_email_domains: '',
|
||||
excluded_email_domains: '', checkin_inactivity_timeout_minutes: '5',
|
||||
})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
|
@ -168,6 +168,20 @@ export default function Settings() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="section-title">Tablet Inactivity Timeout</div>
|
||||
<div className="field" style={{ maxWidth: 200, marginBottom: 20 }}>
|
||||
<label>Minutes before an untouched check-in auto-cancels</label>
|
||||
<input
|
||||
type="number" min={1} max={60} value={settings.checkin_inactivity_timeout_minutes}
|
||||
onChange={e => setSettings(s => ({ ...s, checkin_inactivity_timeout_minutes: e.target.value }))}
|
||||
/>
|
||||
<p className="field-hint">
|
||||
If a guest starts a check-in on the tablet and stops touching the screen for this long, it
|
||||
auto-cancels and returns to the welcome screen — same safety net as the Cancel button and a
|
||||
staff-triggered Reset, just on a timer instead of a tap.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<button className="btn btn-primary" disabled={saving} onClick={handleSave}>
|
||||
{saving ? 'Saving…' : 'Save Settings'}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,12 @@ import type { CheckinTokenData } from '../../types'
|
|||
|
||||
// Safety-net timers per project_checkin's "Multi-tablet device registry" —
|
||||
// busy=hard-reject means an abandoned session must not wedge a tablet
|
||||
// forever. Two nets: this inactivity auto-reset, and the explicit Cancel
|
||||
// button always visible below.
|
||||
const INACTIVITY_MS = 5 * 60 * 1000
|
||||
// forever. Two nets: this inactivity auto-reset (duration admin-configurable
|
||||
// via Settings -> data.inactivity_timeout_minutes, was hardcoded to 5 until
|
||||
// 2026-10-09), and the explicit Cancel button always visible below. A third
|
||||
// net — a server-pushed reset forcing the tablet back to idle even mid-flow
|
||||
// — lives in Kiosk.tsx's SSE 'idle' handler, not here.
|
||||
const DEFAULT_INACTIVITY_MINUTES = 5
|
||||
const COMPLETE_AUTO_RETURN_MS = 15 * 1000
|
||||
|
||||
// 3 real steps (down from 5, 2026-09-01) — contact/car-reg/marketing merged
|
||||
|
|
@ -57,9 +60,10 @@ export default function CheckinFlow({ token, deviceId, onDone }: { token: string
|
|||
// Inactivity auto-cancel — any pointer/keyboard activity resets the timer.
|
||||
useEffect(() => {
|
||||
if (step === 'loading' || step === 'error' || step === 'complete') return
|
||||
const timeoutMs = (data?.inactivity_timeout_minutes || DEFAULT_INACTIVITY_MINUTES) * 60 * 1000
|
||||
function reset() {
|
||||
if (idleTimer.current) clearTimeout(idleTimer.current)
|
||||
idleTimer.current = setTimeout(() => { handleCancel() }, INACTIVITY_MS)
|
||||
idleTimer.current = setTimeout(() => { handleCancel() }, timeoutMs)
|
||||
}
|
||||
const events = ['pointerdown', 'keydown', 'touchstart'] as const
|
||||
events.forEach(e => window.addEventListener(e, reset))
|
||||
|
|
@ -69,7 +73,7 @@ export default function CheckinFlow({ token, deviceId, onDone }: { token: string
|
|||
events.forEach(e => window.removeEventListener(e, reset))
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [step])
|
||||
}, [step, data?.inactivity_timeout_minutes])
|
||||
|
||||
useEffect(() => {
|
||||
if (step !== 'complete') return
|
||||
|
|
|
|||
|
|
@ -96,9 +96,21 @@ export default function Kiosk() {
|
|||
|
||||
useEffect(() => { registerAndGoIdle() }, [])
|
||||
|
||||
// Idle-page SSE — holds open while view === 'idle', reconnects on drop.
|
||||
// Device-level SSE — holds open for as long as this tablet is registered,
|
||||
// not just while view === 'idle'. Previously this closed the moment a push
|
||||
// arrived (view -> 'checkin'), which meant a server-initiated reset (the
|
||||
// extension/Terminals popup's "Reset" button, or the Devices admin page's
|
||||
// test-push reset) had nothing listening on the tablet side while it was
|
||||
// mid-flow — the device flipped back to idle server-side, but the tablet
|
||||
// itself just sat on whatever CheckinFlow step it was on until its own
|
||||
// inactivity timer eventually fired or someone walked over and tapped
|
||||
// Cancel. Keeping one connection open across both views means an 'idle'
|
||||
// event forces the tablet straight back to the welcome screen immediately,
|
||||
// same as a local Cancel — whatever the guest had filled in is simply
|
||||
// discarded, consistent with the existing hard-reject-not-queue design (see
|
||||
// project_checkin's "Multi-tablet device registry").
|
||||
useEffect(() => {
|
||||
if (view !== 'idle' || !device) return
|
||||
if (!device) return
|
||||
|
||||
const es = new EventSource(kioskStreamUrl(device.device_id))
|
||||
esRef.current = es
|
||||
|
|
@ -111,11 +123,11 @@ export default function Kiosk() {
|
|||
}
|
||||
} catch { /* ignore malformed event */ }
|
||||
})
|
||||
es.addEventListener('idle', () => setActiveToken(null))
|
||||
es.addEventListener('idle', () => { setActiveToken(null); setView('idle') })
|
||||
es.onerror = () => { /* browser auto-reconnects EventSource on its own */ }
|
||||
|
||||
return () => { es.close(); esRef.current = null }
|
||||
}, [view, device])
|
||||
}, [device])
|
||||
|
||||
// Fallback heartbeat, independent of whether the SSE connection happens to
|
||||
// be up at any given moment — keeps devices.last_seen fresh for the admin
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ export interface AppSettings {
|
|||
online_checkin_complete_custom_field_id: string
|
||||
terms_and_conditions_text: string
|
||||
excluded_email_domains: string
|
||||
checkin_inactivity_timeout_minutes: string
|
||||
}
|
||||
|
||||
// ── Guest-facing token flow (routes/checkin.js) ─────────────────────────────
|
||||
|
|
@ -121,6 +122,7 @@ export interface CheckinTokenData {
|
|||
booking: CheckinBooking
|
||||
guest: CheckinGuest
|
||||
terms_text: string
|
||||
inactivity_timeout_minutes: number
|
||||
}
|
||||
|
||||
export interface SubmitCheckinPayload {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue