diff --git a/frontend/src/pages/kiosk/Kiosk.tsx b/frontend/src/pages/kiosk/Kiosk.tsx index 7137a61..d7ecae2 100644 --- a/frontend/src/pages/kiosk/Kiosk.tsx +++ b/frontend/src/pages/kiosk/Kiosk.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react' import { UserCheck } from 'lucide-react' -import { kioskLogin, kioskRegisterDevice, kioskHeartbeat, kioskStreamUrl, KioskSessionError } from '../../api' +import { kioskLogin, kioskRegisterDevice, kioskHeartbeat, kioskStreamUrl, kioskReset, KioskSessionError } from '../../api' import type { Device } from '../../types' import CheckinFlow from './CheckinFlow' @@ -48,12 +48,26 @@ export default function Kiosk() { const storedId = localStorage.getItem(DEVICE_ID_KEY) const d = await kioskRegisterDevice(storedId) localStorage.setItem(DEVICE_ID_KEY, d.device_id) + + // This effect only runs on a genuine fresh mount (empty-deps useEffect + // below) — a browser refresh, crash, or power cycle. If the device + // comes back 'busy' at exactly this moment, nothing on THIS browser + // is actually mid-checkin (React state was just initialized), so + // whatever 'busy' the server thinks is stale by definition — the + // in-flow Cancel button only fires its own reset on a clean unmount, + // which a raw page refresh never triggers at all. Self-correct here + // rather than leaving the device invisibly locked with no route back + // except a manual admin reset (hit for real 2026-09-01, see + // project_checkin memory — refreshing mid-checkin instead of using + // Cancel). Best-effort: even if the reset call itself fails, still + // show idle locally so the tablet is at least usable again once + // staff/an admin clears it some other way. + if (d.status === 'busy') { + await kioskReset(d.device_id).catch(() => {}) + d.status = 'idle' + } + setDevice(d) - // Always land on the idle screen regardless of the device's last known - // status — if it was left 'busy' from a session that never completed - // (tablet lost power mid-checkin), the idle screen's own SSE - // connection is what would receive a fresh 'navigate' push anyway, and - // a stuck busy device is fixed from the Devices admin page, not here. setView('idle') } catch (err) { if (err instanceof KioskSessionError) {