Sign states
A sign is in exactly one of five states at any moment. This page is the lookup reference — colors, meanings, transition triggers, and the heartbeat thresholds that drive automated transitions.
For narrative explanations of how monitoring works, see Monitoring & health.
State table
| State | Hex | Meaning | Lifetime |
|---|---|---|---|
| online | #22c55e | Connected and reporting heartbeats | Steady-state for a healthy claimed sign |
| offline | #6b7280 | No heartbeat received within the timeout window | Transient — recovers when heartbeats resume |
| error | #ef4444 | Backend's view that the kiosk's content URL is unreachable, inferred from sign:content_unreachable events emitted by the kiosk's reachability monitor | Transient — clears when the kiosk emits sign:content_recovered |
| maintenance | #f59e0b | Manually placed in maintenance mode | Operator-controlled; cleared on next reboot |
| unlinked | #eab308 | Sign DB record exists but no physical device is linked | Until the record is linked or deleted |
Color semantics:
- Green (online): everything working
- Gray (offline): absence of signal — not necessarily broken, just not heard from
- Red (error): active failure requiring attention
- Amber (maintenance): intentionally out of service
- Yellow (unlinked): needs action, but not broken
Transition rules
┌──────────────────────┐
│ unlinked │
│ (DB record, no │
│ device claimed) │
└──────┬───────────────┘
│ link/claim
↓
┌──────────────────────────────────────────┐
│ online │
└─┬──────────┬──────────┬─────────┬────────┘
│ │ │ │
│ │ │ │ Ctrl+Shift+Q on kiosk,
│ │ │ │ or .maintenance sentinel
│ │ │ ↓
│ │ │ ┌─────────────────┐
│ │ │ │ maintenance │
│ │ │ └────────┬────────┘
│ │ │ │ reboot, sentinel removed
│ │ │ ↓
│ │ │ online
│ │ │
│ │ kiosk emits sign:content_unreachable
│ ↓
│ ┌──────────┐
│ │ error │
│ └────┬─────┘
│ │ kiosk emits sign:content_recovered
│ ↓
│ online
│
│ no heartbeat for 15s+ (3 missed beats)
↓
┌──────────┐
│ offline │
└────┬─────┘
│ heartbeat resumes
↓
online
A sign can also transition to unlinked at any time when an admin unregisters or deletes the device from the dashboard.
Online ↔ offline thresholds
| Property | Value |
|---|---|
| Heartbeat interval | Every 5 seconds |
| Heartbeat TTL in Redis | 30 seconds |
| Background sweep cadence | Every 10 seconds |
| Offline transition | After 15 seconds with no heartbeat (3 missed beats) |
| Online transition | First heartbeat received |
| Worst-case time-to-offline | ~25 seconds (15 s for TTL expiry + up to 10 s sweep delay) |
| Notification deferral | 60 seconds after offline transition |
The 60 s notification deferral means a sign that goes offline and comes back within ~75 s total never notifies anyone. The dashboard still reflects the brief offline blip in the audit log.
Cadence is fixed. The 5-second emit interval and the 60-second notification deferral are product defaults; they aren't customer-tunable per-kiosk. Backend ping/pong cadences are deployment-side settings. If you're investigating disconnect timing on a specific kiosk, the lever is venue-side network hygiene (firewall keepalive, AP stability), not these cadences.
How each state is determined
The kiosk's heartbeat carries a fixed status: 'online'. Other states are derived backend-side from auxiliary signals:
| Backend has heartbeats? | Other signal | Resulting dashboard state |
|---|---|---|
| Yes | (default) | online |
| No (last >15 s) | — | offline |
| Yes | sign:content_unreachable event received | error |
| Yes | .maintenance sentinel detected on the kiosk, or operator-set | maintenance |
| — | No device is linked to the Sign record | unlinked |
The kiosk cannot self-report offline, error, or maintenance in its heartbeat — those are backend-derived conclusions.
Orthogonal mode field
In addition to the five states above, heartbeats carry an orthogonal mode field:
| Heartbeat field | Values | Meaning |
|---|---|---|
mode | 'active' (default) | 'monitoring' | Whether the sign is in monitoring mode — display hidden, audio muted, watchdogs paused, telemetry continues |
This isn't a new state — a sign can be online + mode monitoring (running normally but display intentionally dark) or online + mode active (running and displaying). The dashboard surfaces mode: 'monitoring' as a Monitoring badge alongside the state color.
States that trigger notifications
Per the Notifications defaults:
| Transition | Default channels (if event-subscribed) |
|---|---|
| online → offline | In-app + push (+ email after 60 s defer) |
| offline → online | In-app + push |
| online → error | In-app + push + email |
| error → online | In-app + push |
| → unlinked | In-app + push (event subscribers) |
Notification types
| Type | Trigger | Channels | Filter key | Notes |
|---|---|---|---|---|
sign_offline | Status flips to offline after 60s defer | in-app, email, push | signOffline | 60s defer prevents flap-spam |
sign_error | Status flips to error | in-app, email, push | signError | No defer — error is rare and should fire fast |
sign_online | Status recovers from offline or error AFTER notification was delivered | in-app, email, push | signOnline | Off by default; opt-in per subscription |
content_unreachable | Content URL fails reachability check, 60s defer, delivered-key tracking | in-app, email | signOffline | Bundled under the signOffline filter |
content_recovered | Content URL recovers AFTER unreachable was delivered | in-app, email | signOffline | Same filter as unreachable; only fires if delivered-key was set |
network_failover | Active interface switches (ethernet ↔ wifi) | in-app | signOffline | Single-channel; not email/push |
The filter key is the dashboard's per-event subscription toggle. Subscribing to signOffline covers sign_offline, content_unreachable, content_recovered, and network_failover — the four "offline-flavored" notifications.
State persistence
Sign state is not persistent across DisplaySync deploys — Redis holds the latest heartbeat with a 30 s TTL. If the backend restarts or Redis flushes, signs all transition to offline briefly until each kiosk emits its next heartbeat (within 5 s).
The sign's historical state transitions live in the audit log indefinitely, scoped per-event.
Source of truth
Sign-state colors and names are part of the product surface — the dashboard, mobile app, and these docs all derive from the same canonical list. If you see a state name or color that disagrees with what your dashboard shows, the dashboard is authoritative.
Pre-claim lifecycle
Before a sign reaches one of the five states above, it goes through a brief pre-claim lifecycle managed in Redis (not the SQL Sign records table).
| Stage | Storage | Lifetime | Triggered by |
|---|---|---|---|
| Pending | Redis ws:pending-sign:<shortCode> | 24h TTL | Kiosk emits sign:register on first WebSocket connect |
| Handshake in-flight | Redis linkRequest:<requestId> | 30s TTL | Operator submits claim; backend awaits kiosk link_ack |
| Claimed | SQL Sign record + Redis heartbeat key | Persistent | Kiosk emits link_ack success; backend commits |
| Unclaimed | (nothing — record cleared) | — | Operator unregisters; kiosk emits sign:unregistered |
The handshake-in-flight stage is part of the acknowledged claim flow when LINK_HANDSHAKE_ENABLED is enabled. See Claiming signs → How a claim is committed.
See also
- Monitoring & health — the narrative companion to this reference
- Sign won't claim — common state-transition issues at claim time
- Sign shows offline — diagnosing unwanted offline transitions