Offline for {{ offlineFor || '0 s' }} Syncing {{ state.counts.failed }} failed — needs attention Storage warning Punches are saved on this tablet and will sync automatically when the connection returns. Uploading metadata first, then photos. Nothing to do. Open Device & sync status and use Retry uploads. Evidence is kept on the tablet. Free space on this tablet is running low — sync and ask an admin before it fills up.
{{ state.boot && state.boot.tenant ? state.boot.tenant.name : 'actionly.ca' }} {{ state.boot.device.name }} · {{ state.boot.location ? state.boot.location.timezone : tz() }} Tablet kiosk · not activated
{{ headerTime }}
{{ headerDate }}

Opening local queue…

Activate this tablet

An admin creates an activation code under Locations & devices in the portal. Enter it here once; the device token is then kept on this tablet.

{{ state.conn.online ? 'Connected' : 'No connection — activation needs the server' }} Token stored on device only
Demo devices (Maple Street Market)

{{ unsynced }} punch{{ unsynced === 1 ? '' : 'es' }} from a previous activation are still on this tablet and will sync once a device is active again.

{{ state.activation.error }}

Activation calls POST /api/kiosk/activate; the token is then sent as X-Device-Token on every request. A deactivated device cannot submit new authorised punches.

Enter your PIN

Use your 4–6 digit attendance PIN. A photo will be taken when you clock in or out.

{{ state.pin.digits.length >= i ? '•' : '' }}
{{ state.conn.online ? 'Online' : 'Offline' }} · cached {{ rulesLabel }} {{ state.boot ? state.boot.employees.length + ' PIN verifiers cached' : 'No cache' }} Cache older than {{ (state.boot && state.boot.cache_max_age_hours) || 72 }} h — connect to refresh
{{ state.pin.error }} · {{ lockSeconds }} s
No employee cache yet. {{ state.bootError ? 'Last error: ' + state.bootError : 'Waiting for the server…' }}

Forgot your PIN? Ask a manager to reset it in the portal. PINs are never shown on this screen.

LIVE · fresh photo required
SIMULATED CAMERA · fresh photo required
Starting camera…
Keep your face inside the guide — the photo is taken the moment you tap a button
Camera ready · front camera Simulated camera{{ state.cam.error ? ' · ' + state.cam.error : '' }} Stored on this tablet until synced

Photos are attendance evidence, visible only to authorised managers. A punch cannot be saved without one — if the camera fails, a manager must record an exception in the portal (never a silent bypass).

{{ state.session.allowed[0] === 'clock_in' ? 'Ready to clock in?' : state.session.open && state.session.open.on_break ? 'Back from your break?' : 'Ready to clock out?' }}

{{ state.session.employee.initials }}
{{ state.session.employee.first_name }} {{ state.session.employee.last_name }}
{{ (state.session.employee.positions || []).join(', ') || 'Employee' }}
{{ state.session.window.text }} {{ state.session.source === 'server' ? 'Checked with server' : 'Checked on device' }} · {{ state.boot.rules_version || 'rules' }} cached
Open entry since {{ fmt.time(state.session.open.clock_in_at, tz()) }}{{ state.session.open.on_break ? ' · on break' : '' }}{{ state.session.open.local ? ' · captured on this tablet, not yet synced' : '' }}
Not allowed right now — {{ state.session.refusal }}. Ask a manager if this is wrong; nothing was recorded.
{{ state.captureError }}

Duplicate check: {{ state.session.open ? 'an entry is open for this employee — a second clock-in is disabled here' : 'no clock-in is open on this tablet or in the last sync' }}. Any duplicate found on another tablet after sync goes to manager review — nothing is discarded.

Screen returns to PIN in {{ state.idle }} s if nothing is chosen

{{ fmt.doneLabel(state.confirm.type) }} at {{ state.confirm.time }}

{{ state.confirm.subtitle }}

Photo captured Synced · photo uploaded{{ confirmEvent.state === 'review' ? ' · flagged for manager review' : '' }} Saved offline — {{ unsynced }} punch{{ unsynced === 1 ? '' : 'es' }} pending sync Saved on device · syncing Kept on device until the server confirms punch and photo
1 · Saved on tablet→ 2 · Metadata {{ confirmEvent && confirmEvent.server_status === 'duplicate' ? 'duplicate' : confirmEvent && confirmEvent.server_status === 'review' ? 'review' : 'accepted' }}→ 3 · Photo uploaded→ 4 · Complete

{{ state.confirm.type === 'clock_out' ? 'See you next shift, ' + state.confirm.first_name + '.' : state.confirm.type === 'break_start' ? 'Enjoy your break, ' + state.confirm.first_name + '.' : 'Have a good shift, ' + state.confirm.first_name + '.' }}

Remember to record your break on this tablet. {{ state.confirm.deadline }}

Wrong person or wrong button? Tell a manager. Punches are never deleted on the tablet — a manager resolves it in the portal with an audit trail.

{{ confirmEvent.event_id }} · captured {{ fmt.time(confirmEvent.captured_at, tz(), true) }} device time · {{ state.boot.device ? state.boot.device.name : 'device' }} · {{ confirmEvent.rules_version || 'rules' }} · {{ tz() }} ({{ fmt.offsetLabel(confirmEvent.captured_offset_minutes) }}) · clock drift {{ clockDrift == null ? 'unknown' : (clockDrift >= 0 ? '+' : '') + clockDrift + ' s at last anchor' }}
Clearing screen in {{ state.confirm.seconds }} s — your name and photo are removed from the display for privacy

Device & sync status

{{ state.boot && state.boot.device ? state.boot.device.name + ' · ' + state.boot.device.id : 'device' }} · this screen shows no employee photos

Connection

{{ state.conn.online ? 'Online' : 'Offline for ' + (offlineFor || '0 s') }}
Since{{ state.conn.offline_since ? fmt.time(state.conn.offline_since, tz()) : '—' }} · last successful sync {{ state.conn.last_sync_at ? fmt.time(state.conn.last_sync_at, tz()) : 'never' }}
Network{{ state.simulateOffline ? 'Simulated offline (toggle in footer)' : navigator.onLine === false ? 'Browser reports no network' : 'Browser online' }}
Retries{{ state.conn.retries }} attempt{{ state.conn.retries === 1 ? '' : 's' }}
Last error{{ state.conn.last_error || 'none' }}
PunchesStill accepted offline

Sync queue

{{ state.counts.pending }}Pending sync
{{ state.counts.metadata_accepted }}Metadata accepted
{{ state.counts.photo_pending }}Photo pending
{{ state.counts.failed }}Failed
{{ state.counts.complete_today }}Complete today
Oldest pending{{ fmt.time(state.counts.oldest_pending.captured_at, tz()) }} · {{ state.counts.oldest_pending.employee_name }} · {{ fmt.duration(state.now - Date.parse(state.counts.oldest_pending.captured_at)) }}—
Review required after sync{{ state.counts.review }}

Storage & app

Browser storage{{ state.storage.usage != null ? fmt.bytes(state.storage.usage) + ' of ' + fmt.bytes(state.storage.quota) + ' · ' + state.storage.pct + '%' : 'estimate unavailable' }}{{ storageWarn ? ' · warning above 80%' : ' · warns above 80%' }}
Pending evidence{{ fmt.bytes(state.counts.queued_bytes) }} · {{ unsynced }} punches · {{ state.counts.photo_pending }} photos
Local storageIndexedDB · survives app restart
Restart self-test ·
App{{ APP_VERSION }} · web kiosk

Events on this device · {{ state.events.length }} kept (complete rows pruned after 7 days) · newest first

Event idEmployeeTypeCaptured (device)Server receiptClock offsetPhotoState
{{ e.event_id.slice(0, 13) }}… {{ e.employee_name }} {{ fmt.typeLabel(e.type) }}offline {{ fmt.shortDT(e.captured_at, tz(), true) }} {{ e.received_at ? fmt.shortDT(e.received_at, tz(), true) : '—' }} {{ e.clock_offset_seconds == null ? '— (no anchor)' : (e.clock_offset_seconds >= 0 ? '+' : '') + e.clock_offset_seconds + ' s' }} {{ e.photo_state }} {{ fmt.stateLabel(e) }}
{{ e.last_error }}
{{ e.reason }}
No events captured on this device yet
Evidence is retained until the server acknowledges both metadata and photo. Retries reuse the same event id, so a punch is never duplicated. Duplicates across tablets, stale permissions and clock anomalies are flagged for a manager — this device never picks a winner.

Cached data

Employees{{ state.boot ? state.boot.employees.length : 0 }} · {{ state.boot && state.boot.location ? state.boot.location.name : '—' }} · updated {{ state.bootFetchedAt ? fmt.time(state.bootFetchedAt, tz()) : 'never' }} ({{ cacheAge }} ago)
PIN verifiers{{ state.boot ? state.boot.employees.filter(e => e.pin_verifier).length : 0 }} hashed · never shown
Schedules{{ state.boot ? state.boot.shifts.length + ' shifts cached (yesterday – tomorrow)' : '—' }}
Open entries{{ state.boot ? state.boot.open_entries.length : 0 }} at last sync
Attendance rules{{ rulesLabel }}
Cached access lifetime{{ (state.boot && state.boot.cache_max_age_hours) || 72 }} h{{ cacheStale ? ' · exceeded' : '' }} · refresh every 15 min online
Source{{ state.bootSource === 'server' ? 'server (this session)' : state.bootSource === 'cache' ? 'IndexedDB cache' : '—' }} · last refresh failed: {{ state.bootError }}

Trusted time

Last trusted anchor{{ state.heartbeat ? fmt.time(state.heartbeat.server_time, tz(), true) + ' server time' : 'none yet' }}
Device clock at anchor{{ state.heartbeat ? fmt.time(state.heartbeat.device_clock_at, tz(), true) : '—' }}
Device clock drift{{ driftLabel }}
RuleDrift over 15 min → punch goes to review (server); over 60 s shown here as drifting
LimitationOffline, this tablet cannot know changes made after {{ state.bootFetchedAt ? fmt.time(state.bootFetchedAt, tz()) : 'the last sync' }}

Recovery notes

Camera{{ state.cam.error ? state.cam.error + ' · simulated fallback' : 'OK or simulated on demand' }}
Corrupt / missing photos{{ state.events.filter(e => e.photo_state === 'failed').length }} · a bad file marks the punch failed, never silently skipped
Forgotten PINManager resets it in the portal (audited)
RiskClearing browser data or losing the tablet loses unsynced evidence
Device

This device was deactivated

It cannot submit new authorised punches. {{ unsynced }} unsynced punch{{ unsynced === 1 ? '' : 'es' }} captured earlier stay on this tablet and are quarantined for manager review — nothing is discarded.

Device ID {{ state.boot && state.boot.device ? state.boot.device.id : '—' }} Last sync {{ state.conn.last_sync_at ? fmt.time(state.conn.last_sync_at, tz()) : 'never' }} App {{ APP_VERSION }} Clock drift {{ (state.heartbeat.clock_offset_seconds >= 0 ? '+' : '') + state.heartbeat.clock_offset_seconds }} s {{ state.screen === 'sync' ? 'Back to PIN' : 'Device & sync status' }}
{{ state.toast }}