Ox Dot
Living snapshot of the SageOx Ox Dot device UI. Reading order is Surfaces -> Components -> Tokens, per the catalog contract. Snapshot-only — git history of this file is the time machine.
All surface catalogs · Design history · ox-dot-v2 (Apr 2026)
Surfaces
58 entriesWhole-screen flows the user actually experiences. Captured from the live device firmware framebuffer (or the canonical v2 design pass as a fallback). Variants are grouped under their parent screen.
overlay_cast_confirm
overlay_cast_confirmWhen to use. Ask-mode cast prompt: paired TV came online during a session. 'Cast to <TV name>?' header + Yes/No pills. Auto-dismisses after 8s with No.
When not to. Don't show in Always mode (cast::Mode::Replace) — that mode casts implicitly. The overlay is only for Ask mode (cast::Mode::Ask).
screen_auto_logout_countdown
screen_auto_logout_countdownWhen to use. Shared-space mode (kiosk deployments). Counts down ~30s before auto sign-out from inactivity. Tap to keep session alive. Gated on FEATURE_SHARED_SPACE_MODE — not in default build.
When not to. Don't enable in single-user deployments — sign-out friction is unwarranted. Gate at the feature flag, not the screen.
screen_awaiting_confirm
screen_awaiting_confirmWhen to use. Mid-recording 'Still there?' countdown overlay (~10s). Fires when the recorder detects the user has wandered off (no audio activity threshold). Tap to keep recording, ignore to auto-finalize.
When not to. Don't fire too often during quiet meetings — false trigger is worse than missing one. Threshold lives in audio config.
screen_boot
screen_bootWhen to use. First frame on power-up and during any post-restart cycle, before WiFi connects. Three pulsing dots + small status line below tell the user the device is alive while WiFi / NVS / auth come online.
When not to. Don't keep BOOT showing past WiFi handshake — switch to WIFI on failure or IDLE on success. Anything past ~5s on BOOT signals a broken first boot.
screen_cast_control
screen_cast_controlWhen to use. Active cast control: paired TV name, join URL QR (for participant phones), Disconnect pill, X close pill. Pills grey-out during in-flight disconnect operations.
When not to. Don't navigate AWAY from CAST_CONTROL while a stream is active without an explicit Disconnect — leaving silently leaves the TV showing stale content.
screen_cast_control · CONNECTING
screen_cast_control_connectingWhen to use. Cast-control screen during the connect handshake (CastV2 or AirPlay session establishment). Disconnect pill greyed out; status arc shows 'Connecting…'.
When not to. Don't allow taps on Disconnect during connecting — the session lifecycle is non-cancellable until the handshake completes or fails.
screen_cast_test
screen_cast_testWhen to use. One-tap test cast to verify a paired TV works. 30s countdown + 'See SageOx on TV?' prompt + 720/1080/4K resolution segments. Lives under Settings > Cast > Test.
When not to. Don't fire CAST_TEST without a paired receiver — endpoint returns 409 if not paired. Pair-flow is its own overlay.
screen_cast_test · ERROR
screen_cast_test_errorWhen to use. Test-cast failure or timeout. Shows the user-readable failure reason (TV unreachable, app launch failed, manifest rejected) + Retry pill.
When not to. Don't silently retry on cast-test failure — the user explicitly invoked the test and needs to see the cause.
screen_cast_test · SUCCESS
screen_cast_test_successWhen to use. Test-cast success state. Green check + 'Looks good' confirms the user tapped Yes on the countdown. Auto-dismisses to the cast settings page.
When not to. Don't infer success from absence of error — the user must explicitly tap Yes to confirm picture on TV.
screen_done
screen_doneWhen to use. Recording finalized + uploaded. Green checkmark, recording duration, team label, QR code linking to the cloud transcript. Auto-dismisses to IDLE after ~10s or on tap.
When not to. Don't show DONE if the upload failed — error path is ERROR screen with retry affordance.
screen_done · OFFLINE
screen_done_offlineWhen to use. Recording captured but device was offline at finalize — buffered locally, will drain when WiFi returns. QR + cloud link are omitted since transcript isn't live yet.
When not to. Don't show the green checkmark in offline-done — replace with a queued/buffered indicator so the user knows the recording isn't visible in the cloud yet.
screen_error
screen_errorWhen to use. Hard failure that needs the user's attention (auth lost, no team, repeated upload failure). Shows the user-readable message + a 'Try again' pill. Auto-dismisses after 10s.
When not to. Don't use ERROR for transient network blips — silently retry and queue chunks. ERROR is for failures that need a human decision.
screen_factory_reset_confirm
screen_factory_reset_confirmWhen to use. Last gate before nvs_erase + reboot. Header: 'Erase device?'. Detail explicitly names what's lost (WiFi, pairing). Danger pill: 'Erase'. Cancel pill: ghost. Most destructive UI flow.
When not to. Never auto-trigger this overlay — only from Settings > Device after an explicit user navigation.
screen_idle
screen_idleWhen to use. Default ready-to-record state. Ambient ring shows rolling 30-second buffer fill (rewind window). Team name in the lower curve. Single-tap anywhere starts a recording.
When not to. Don't add affordances inside the inner ring (those compete with the implicit 'tap anywhere to record' gesture). Status indicators live in the outer ring.
screen_idle · OFFLINE
screen_idle_offlineWhen to use. WiFi is down. Same idle layout + a small offline badge in the outer ring. Recording still works (captures locally + drains on reconnect). User gets a passive cue, not a modal.
When not to. Don't preempt to the WIFI screen mid-recording — recorder is capture-only and survives offline; only show WIFI from IDLE.
screen_idle · PUBLIC
screen_idle_public_teamWhen to use. Active team is configured as PUBLIC (recordings shared across the org). Teal accent on the team-name lower curve is the canonical 'this is public' indicator.
When not to. Never use teal (CLR_TEAL_500) for anything else on any other screen — it's a reserved semantic for public-team context cueing.
screen_idle · REWIND
screen_idle_rewindWhen to use. Rewind buffer is filling (sage arc growing from 0% to 100% over 30s). Small rewind chevron affordance appears once buffer is full — tap to start a recording that includes the prior 30s.
When not to. Don't hide the rewind arc once full — it's the visual cue that the rewind feature is available. Even when not used, it signals capability.
screen_idle · UPDATE
screen_idle_update_availableWhen to use. OTA manifest reports a newer firmware version. Small badge in the outer ring; the actual prompt happens on next boot (or via Settings > Device > Check for updates).
When not to. Don't interrupt IDLE with a modal — the badge is enough; the user will see it on their schedule.
screen_logout_confirm
screen_logout_confirmWhen to use. Confirmation overlay for the 'Sign Out' action in Settings. Header: 'Sign out?'. Danger pill: 'Sign Out'. Cancel pill: ghost. Sign-out wipes auth tokens but preserves WiFi.
When not to. Don't reuse this overlay for partial sign-outs (e.g., switching teams) — that's a non-destructive Team Picker tap.
screen_pairing
screen_pairingWhen to use. Device-code OAuth pairing flow. Shows the 6-char user code, a QR linking to the verification URL, and a countdown ring for the code's 15-minute TTL. The single load-bearing first-run screen.
When not to. Don't reuse the QR + countdown layout for non-pairing flows — rewind consent and update prompt have their own scaffolds.
screen_pairing · RETRY
screen_pairing_retryWhen to use. Pairing failed (network blip, code expired, OAuth denied). Shows the failure reason + a Retry pill that restarts the device-code flow with a fresh code.
When not to. Don't auto-retry — pairing failure usually means the user took too long; restart only on their explicit tap.
screen_pairing · WAITING
screen_pairing_waitingWhen to use. Post-code-entry intermediate state. User has scanned the QR and entered the code in the browser; firmware is polling the OAuth /device/token endpoint waiting for approval.
When not to. Don't drop the countdown ring here — it still owns the remaining TTL of the device code; if it expires the user has to restart pairing.
screen_record_prompt
screen_record_promptWhen to use. Large 'Record' CTA overlay that appears when the user is about to start a meeting but the device wants explicit confirmation (shared-space mode, or after a long idle).
When not to. Don't show on a tap that's clearly intentional — adds a second tap to the most common interaction. Reserve for ambiguous wake conditions.
screen_recording
screen_recordingWhen to use. Active capture for a PRIVATE team. Red pulsing ring + red background wash + elapsed-time digits (Mont 30 mono). Status arc top reads 'RECORDING'. Tap anywhere to enter the pause/stop sheet.
When not to. Never tint the recording screen any color other than the state-load-bearing red (active) / amber (paused or upload-failing). Status colour is the only state cue from a glance.
screen_recording · CAST OK
screen_recording_cast_reachableWhen to use. Recording with paired TV link healthy. Small TV-OK icon in the outer ring cues that this session is also live-casting.
When not to. Don't add a tap target on the cast icon during recording — the indicator is status-only; cast control lives behind Settings > Cast.
screen_recording · CAST DOWN
screen_recording_cast_unreachableWhen to use. Recording proceeds normally but paired TV is unreachable. Greyed cast icon tells the user the TV is offline; recording itself is unaffected.
When not to. Never block recording on cast failure — cast is best-effort downstream; capture and upload are independent.
screen_recording · PAUSED
screen_recording_pausedWhen to use. Recording paused mid-session. Amber background wash + 'PAUSED' status arc replace the active red. Timer keeps running (elapsed wall-clock) but new audio isn't captured.
When not to. Don't keep recording-paused open indefinitely — auto-resume after ~5 min idle to avoid leaving stale sessions open on shared devices.
screen_recording · PUBLIC
screen_recording_publicWhen to use. Active capture for a PUBLIC team. Teal accents on the lower-curve team label cue that this recording will be shared across the org.
When not to. Don't drop the teal indicator when paused — even at rest, the public visibility status is load-bearing context.
screen_recording · SYNC PENDING
screen_recording_sync_pendingWhen to use. Mid-recording status: chunks have queued faster than the uploader can drain (transient slow WiFi). Small 'Syncing N…' label tells the user upload is behind but recording is still capturing.
When not to. Never make this a modal or interrupt the recorder — the label is informational only; capture is independent of delivery (post-resilience plan §2.8).
screen_recording · UPLOAD FAILING
screen_recording_upload_failingWhen to use. Sustained upload failure (offline > N seconds). Amber background wash + 'Saving locally' label tell the user we're buffering to flash and will drain later.
When not to. Don't stop recording on upload failure — that defeats the purpose of the offline buffer. The amber wash communicates degraded delivery without interrupting capture.
screen_rewind_consent
screen_rewind_consentWhen to use. One-time consent overlay before enabling the 30-second ambient buffer (rewind). Explains what the feature does + the privacy trade-off. Decision is sticky in NVS.
When not to. Don't show on every boot — once acknowledged, the answer persists. Re-show only if storage clears or user explicitly toggles in Settings.
screen_safe_mode
screen_safe_modeWhen to use. Crash-recovery screen. Boot counter detected a panic loop; device drops to a minimal screen with a QR linking to support + an option to retry the OTA. Last-resort UI.
When not to. Never code paths that 'might' trigger safe mode — boot counter is the single source of truth. Triggered only by esp_reset_reason() + sequential panics.
screen_safe_mode · OFFLINE
screen_safe_mode_offlineWhen to use. Safe-mode with WiFi also down — QR with support URL + a static recovery hint ('connect via USB serial'). The lowest-capability state the firmware reaches before bricking.
When not to. Don't substitute a different message — the QR pointing at support docs is the canonical recovery anchor and must remain visible even without WiFi.
screen_settings
screen_settingsWhen to use. Horizontal swipe pager containing every settings subpage. Profile is the default landing page. Pager dots in the outer ring; current page title centred. Reached from IDLE via top-edge swipe down.
When not to. Don't add a 9th page without auditing the pager dot density — the outer ring fits ~8 dots before they crowd the bezel.
screen_settings · ABOUT
screen_settings_aboutWhen to use. About subpage: firmware version, build SHA, copyright line, QR linking to release notes. The 'who am I and what version' page for support triage.
When not to. Don't dump every config knob here — About is identity + version only. Knobs live in the relevant feature subpages.
screen_settings · CAST
screen_settings_castWhen to use. Cast subpage with paired TV: shows TV name, Test pill, Unpair pill, mode toggle (Off / Ask / Always).
When not to. Don't show pair affordances here once paired — pairing is a discrete flow under Settings > Cast > Select TV.
screen_settings · CAST PICKER
screen_settings_cast_pickerWhen to use. Discovered TV list (post-scan). Scrollable rows with TV name + mDNS host. Tap a row to start pairing.
When not to. Don't show the picker before scan completes — show the scanning state instead.
screen_settings · CAST SCANNING
screen_settings_cast_scanningWhen to use. Active mDNS / SSDP scan for cast targets. Spinning ring + 'Scanning…' label + Cancel pill. Transitions to picker on results.
When not to. Don't block scanning longer than ~5s before showing partial results — slow-discovery TVs (DIAL, webOS pre-cache) trickle in over 5-15s.
screen_settings · CAST UNPAIRED
screen_settings_cast_unpairedWhen to use. Cast subpage with no paired TV: shows 'Select TV' CTA + explainer for what cast does. First-run state for this subpage.
When not to. Don't show pair affordances inline — pairing happens in a dedicated picker after the user taps Select TV.
screen_settings · CONSENT
screen_settings_consentWhen to use. Consent toggles: rewind buffer, telemetry, crash reporting. Each row is a yes/no with the privacy implication spelled out. Decision is sticky in NVS.
When not to. Don't bury consent UI behind multiple taps — the page lives at the top level of Settings precisely so it's discoverable.
screen_settings · DEBUG
screen_settings_debugWhen to use. Debug subpage (dev builds only — FEATURE_DEBUG_MODE). QR linking to the device's HTTP debug console at http://sageox-XXXX.local/ for log tailing.
When not to. Never enable the debug subpage in release builds — the debug HTTP server exposes log_ring contents which may contain redacted secret prefixes.
screen_settings · DEVICE
screen_settings_deviceWhen to use. Device subpage: firmware version + 'Check for updates' / 'Download' / 'Reboot Now' CTA cycling per OTA state. Also houses the Factory Reset entry (gated by a confirm sheet).
When not to. Don't expose esoteric flags here (text scale, etc.) — those live in dedicated subpages or are derived from user account.
screen_settings · OTA CHECK
screen_settings_device_check_progressWhen to use. Manual OTA check in progress (CTA: 'Checking…' with spinner). Fires when user taps Check for updates and the device queries the manifest endpoint.
When not to. Don't reuse this layout for any other 'wait for network' action — it's wired to the OTA result transition.
screen_settings · MEMORY
screen_settings_memoryWhen to use. Memory subpage: int / PSRAM free counters + a 'Reservations' drilldown linking to docs/memory/map.md. Dev / support tool, not for end-user consumption.
When not to. Don't show this in single-user release builds without the debug feature flag — it leaks build internals.
screen_settings · PROFILE
screen_settings_profileWhen to use. Profile / Account subpage: user name, email, paired team count, Sign Out pill (which fires the LOGOUT_CONFIRM overlay).
When not to. Don't show profile actions without a paired team — sign-out from a half-paired state would leave the device unauthenticated but with WiFi, which is a useless half-state.
screen_settings_wifi
screen_settings_wifiWhen to use. WiFi subpage: current SSID, IP, MAC + 'Change' pill to enter the network picker (SETTINGS_WIFI list mode).
When not to. Don't show the picker as the default WiFi view — most users never need to change networks; the current-network summary is the right primary view.
screen_settings_wifi · LOCKED
screen_settings_wifi_lockedWhen to use. WiFi-only lock mode: the supervisor has restricted the user to the WiFi settings screen until reconnection. Shows the specific reason that triggered the lock (no recovery known).
When not to. Never preempt to this lock during a live recording — recorder is capture-only and survives offline; lock only fires from IDLE state.
screen_settings_wifi · SCANNING
screen_settings_wifi_scanningWhen to use. Active WiFi network scan. Spinning ring + 'Scanning…' label. Transitions to the network list once the scan completes.
When not to. Don't block longer than ~5s on a scan — show partial results as they arrive.
screen_streaming
screen_streamingWhen to use. Brief 'still uploading' indicator after recording stops but the final chunks haven't drained yet. Shows progress bar + remaining chunk count. Auto-transitions to DONE on flush.
When not to. Don't block the user from starting a new recording during STREAMING — the upload queue is independent from capture.
screen_streaming · CHUNKS PENDING
screen_streaming_chunks_pendingWhen to use. Streaming screen with explicit remaining chunk count — shown when upload is slow (e.g., big recording on degraded WiFi) and the user wants to know progress.
When not to. Don't show chunk count for fast uploads — adds noise to a two-second screen. Threshold for showing the count lives in ui screen logic.
screen_team_picker
screen_team_pickerWhen to use. Horizontal swipe carousel of teams the user belongs to. Each card shows team name + an avatar / colour. Reached from IDLE via the team-name lower-curve tap. Selecting a team updates the recording target for the next session.
When not to. Don't show TEAM_PICKER if the user belongs to only one team — collapse to a no-op and skip the affordance.
screen_update_prompt
screen_update_promptWhen to use. Boot-time OTA prompt: '0.1.0 -> 0.4.6' + 10s countdown ring + release date + Update / Skip pills. Auto-accepts on countdown expiry.
When not to. Don't show during a recording (would interrupt capture). OTA check is gated behind UI_SCREEN_IDLE state.
screen_update_prompt · DOWNLOADING
screen_update_downloadingWhen to use. OTA in progress. Accept pill replaced with 'Downloading…' label; ring fills as the binary streams. Device stays responsive (download is Core 0 task on PSRAM stack).
When not to. Don't reuse this layout for non-OTA downloads — the ring fill is semantically OTA progress and shouldn't be diluted.
screen_update_prompt · FAILED
screen_update_failedWhen to use. OTA download or signature verify failed. Shows the user-readable failure reason + Skip pill. Device remains on current firmware; next boot retries.
When not to. Never auto-retry an OTA from this screen — failed signature verification can mask a real attack vector; user must opt in.
screen_welcome
screen_welcomeWhen to use. Brief post-pairing transition (~2s). Confirms success and hands off to IDLE. Shows the user's name + team affiliation as the first 'we know you' moment.
When not to. Don't extend the welcome screen beyond 2-3 seconds — the user is ready to record; WELCOME is a transitional polish frame, not a destination.
screen_wifi
screen_wifiWhen to use. WiFi association in progress (or after disconnect during runtime). Shows current SSID in copper text + status message. Owns the screen while WiFi is the blocking dependency.
When not to. Don't use as a passive offline indicator on IDLE — that's the offline badge in the corner of the idle screen.
screen_wifi_password
screen_wifi_passwordWhen to use. Password entry for a selected WiFi network. Shows the on-screen keyboard (v2: ABC / abc / 123! tabs + Go / Cancel pills). SSID name above the input.
When not to. Don't reuse for non-WiFi text input — keyboard component is shared but the password screen owns the SSID context and the connect-on-Go callback.
screen_wifi_password · OPTIONS
screen_wifi_network_optionsWhen to use. Per-known-network action sheet (Forget / Reconnect). Reached by long-press on a row in the WiFi network list.
When not to. Don't auto-open for non-known networks — Forget is only meaningful for stored credentials.
Components
0 entriesReusable widgets the surfaces are composed of — pill buttons, rings, list rows, sheets. Each shown in every variant.
This section is currently empty. The Ox Dot firmware exposes all 18 reusable LVGL components, but isolation rendering of each variant requires a firmware POST /debug/catalog/render endpoint that lets the catalog draw one widget on a blank screen and snap it. That endpoint and the per-component factories ship in a follow-up: scribe-bmpc (Phase 2) and scribe-z6yy (Phase 4).
Tokens
6 entriesThe raw values underneath: color palettes, recording-surface tints, semantic role colors, and the safe-zone gesture model.
palette_charcoal
palette_charcoalWhen to use. Backgrounds, cards, borders, text on light surfaces. Charcoal.900 is the device dark-mode base; Charcoal.800 is the surface tint.
When not to. Don't use mid stops (400-600) for body text — contrast falls below WCAG AA against the dark base.
palette_copper
palette_copperWhen to use. CTA accents (Copper.400 on device), hero highlights. Max one copper accent per screen.
When not to. Not for warnings — those are Amber (semantic.warn.500). Not for danger — that's semantic.danger.500.
palette_recording_tints
palette_recording_tintsWhen to use. Full-screen background washes during recording states. bg_active = live capture; bg_warn = upload-failing / offline-buffering.
When not to. Don't tint anything outside the recording screen with these — they're state-load-bearing.
palette_sage
palette_sageWhen to use. Primary brand surfaces, active states, ring fills. Sage.500 is the canonical brand token.
When not to. Never use as a CTA accent — that role belongs to Copper.400. Avoid Sage on top of Sage at adjacent stops (low contrast).
palette_semantic
palette_semanticWhen to use. Use the semantic role, not the underlying hex. public.500 (teal) is RESERVED for the public-recording indicator and must not appear elsewhere.
When not to. Don't substitute Copper for amber (warn) — they read as the same hue on small targets and conflate CTA with warning.
safe_zone_diagram
safe_zone_diagramWhen to use. Round 360x360 display. Place all interactive elements within the 136 px safe radius (24 px inset from the LCD edge). Core zone (0-80 px) is primary tap; mid ring (80-130 px) is swipe affordance; outer ring (130-180 px) is for non-interactive status only.
When not to. Don't put taps inside 24 px of the LCD edge — the bezel curvature and finger-pad geometry make them unreliable. Tap targets must be >= 44 px and never closer than 24 px to each other.