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:
jtricerolph 2026-10-09 12:19:53 +00:00
parent 790431c446
commit 5180adf8d2
10 changed files with 84 additions and 14 deletions

View file

@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Send to Check-In Tablet",
"version": "0.2.0",
"version": "0.2.1",
"description": "Pushes the currently open NewBook booking to a check-in tablet — standalone from NewBook Assistant, see project_checkin memory.",
"permissions": ["cookies", "storage", "activeTab", "webNavigation"],

View file

@ -214,6 +214,12 @@ function renderTablets(el, devices, baseUrl, apiKey) {
el.querySelectorAll('.reset-btn').forEach(btn => {
btn.addEventListener('click', async () => {
// Reset now pushes a live 'idle' event straight to the tablet
// (2026-10-09, see tablet-checkin's Kiosk.tsx) rather than only
// flipping its status server-side — 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.
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;
btn.disabled = true;
btn.textContent = '…';
try {

View file

@ -130,6 +130,11 @@ const SETTINGS_DEFAULTS = {
// project_checkin_legacy_plugin) but editable from Settings so any other
// OTA/agent domain can be added without a code change.
excluded_email_domains: 'guest.booking.com,m.expediapartnercentral.com',
// Minutes of no touch/keyboard activity on the tablet's check-in flow
// before it auto-cancels and returns to the welcome screen — see
// CheckinFlow.tsx's inactivity timer, previously hardcoded to 5 and now
// admin-tunable from Settings.
checkin_inactivity_timeout_minutes: '5',
}
async function seedSettings() {

View file

@ -8,7 +8,6 @@ import {
import { generateRegcardPdf } from '../lib/pdf.js'
import { saveRegcardPdf } from '../lib/uploads.js'
import { queueMarketingSync } from '../lib/marketing/index.js'
import { pushIdle } from '../lib/device-stream.js'
const DEFAULT_TERMS = 'No terms & conditions text has been configured yet — set one under Settings.'
@ -66,6 +65,7 @@ export async function checkinRoutes(app) {
car_regos: existingCarRegos,
},
terms_text: await getSetting('terms_and_conditions_text', DEFAULT_TERMS),
inactivity_timeout_minutes: Number(await getSetting('checkin_inactivity_timeout_minutes', '5')) || 5,
}
})
@ -180,8 +180,14 @@ export async function checkinRoutes(app) {
}
// Step 7 — flip the device back to idle so the next guest can be pushed.
// Deliberately no pushIdle() here, unlike lib/push.js's resetDevice() —
// this is the same browser that's already showing its own confirmation
// screen and will return to idle on its own timer (see CheckinFlow.tsx's
// COMPLETE_AUTO_RETURN_MS); forcing the SSE 'idle' event here as well
// would race that confirmation screen and could skip straight past it
// the moment Kiosk.tsx started listening for 'idle' across the whole
// session rather than only while idle (2026-10-09, see Kiosk.tsx).
await pool.query(`UPDATE devices SET status = 'idle' WHERE device_id = $1`, [session.device_id])
pushIdle(session.device_id)
return { ok: true, checkin_id: checkin.id, pdf_generated: checkin.status === 'complete' }
})
@ -219,8 +225,12 @@ async function failCheckin(session, errorMessage) {
VALUES ($1, NULL, $2, $3, 'failed', $4)`,
[session.booking_id, session.hotel_id, session.device_id, errorMessage]
)
// No pushIdle() here either, same reasoning as the success path above —
// the guest/tablet is shown an error on this same session (see
// CheckinFlow's catch block, which stays on the 'terms' step rather than
// calling onDone()) and forcing an SSE 'idle' nudge would yank the tablet
// away from that error message instead of letting staff see it.
await pool.query(`UPDATE devices SET status = 'idle' WHERE device_id = $1`, [session.device_id])
pushIdle(session.device_id)
}
async function retry(fn, times) {

View file

@ -11,6 +11,7 @@ import { testConnection } from '../lib/newbook.js'
const ALLOWED_KEYS = [
'regcard_custom_field_id', 'online_checkin_complete_custom_field_id',
'terms_and_conditions_text', 'excluded_email_domains',
'checkin_inactivity_timeout_minutes',
]
export async function settingsRoutes(app) {
@ -28,6 +29,16 @@ export async function settingsRoutes(app) {
for (const key of Object.keys(body)) {
if (!ALLOWED_KEYS.includes(key)) return reply.status(400).send({ error: `Unknown or protected setting: ${key}` })
}
// Guarded separately from the generic string pass-through below — this
// one directly controls the tablet's inactivity safety-net timer (see
// CheckinFlow.tsx), so a stray "0"/non-numeric value here shouldn't be
// able to make every check-in auto-cancel instantly.
if ('checkin_inactivity_timeout_minutes' in body) {
const n = Number(body.checkin_inactivity_timeout_minutes)
if (!Number.isFinite(n) || n < 1 || n > 60) {
return reply.status(400).send({ error: 'Inactivity timeout must be a number of minutes between 1 and 60' })
}
}
for (const [key, value] of Object.entries(body)) {
await setSetting(key, String(value ?? ''))
}