archy/neode-ui/docs/GAMEPAD-NAV-MAP.md
Dorian 64b57dca7d fix: overhaul container lifecycle — recovery, health, uninstall, UI state
Container recovery:
- Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s
- Dependency-aware restarts: won't restart services before their deps
- Reset dependent counters when a dependency recovers
- Handle "created" state containers (were invisible to health monitor)
- Added IndeedHub, mempool-api, mysql to tier system
- Crash recovery: podman start timeout 30s→120s with retry
- Podman client: socket timeout 5s→30s, added restart policy

UI state representation:
- Exit code 0 shows "stopped" (gray), not "crashed" (red)
- Exit code 137 shows "killed (OOM)"
- Non-zero exit shows "crashed" (red)
- Added exit_code field to PackageDataEntry

Install/uninstall fixes:
- Install returns error when container doesn't start (was silent success)
- Post-install hooks awaited instead of fire-and-forget tokio::spawn
- Uninstall: graceful rm before force, volume prune, network cleanup
- Uninstall returns error on partial failure (was 200 OK)

Config consistency:
- DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded)
- Bitcoin: added ZMQ ports 28332/28333 for LND block notifications
- IndeedHub port 7777→8190 (was conflicting with strfry)
- Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0

Performance:
- Metrics collector interval 60s→300s (was duplicating health monitor)
- Podman client: proper error propagation instead of unwrap_or_default

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:03:57 +01:00

30 KiB

Gamepad Navigation Map

Every arrow key, every position, every page.

[C] = Container (red tile, D-pad grid) [N] = Nav bar item (secondary, reached via Up from top row) [Y] = Inner control (entered via Enter on container, exited via Escape) [S] = Sidebar item


Sidebar (all pages)

Vertical list. Up/Down wrap. Right enters page. Left does nothing.

Position Up Down Right Left
Home Logout Apps First [C] nothing
Apps Home Cloud First [C] nothing
Cloud Apps Mesh First [C] nothing
Mesh Cloud Network First [C] nothing
Network Mesh Web5 First [C] nothing
Web5 Network Fleet First [C] nothing
Fleet Web5 Settings First [C] nothing
Settings Fleet AIUI First [C] nothing
AIUI Settings Logout First [C] nothing
Logout AIUI Home First [C] nothing

HOME /dashboard

Nav bar [N]

[N] Dashboard    [N] Setup

Grid [C]

Row 1:  [C] My Apps       [C] Cloud
Row 2:  [C] Network       [C] Wallet
Row 3:  [C] System
Row 4:  [C] Quick Start   (full-width, if visible)
Position Up Down Left Right Enter
[N] Dashboard nothing My Apps nothing Setup Switch tab
[N] Setup nothing My Apps Dashboard nothing Switch tab
My Apps [N] bar Network Sidebar Cloud /dashboard/apps
Cloud [N] bar Wallet My Apps nothing /dashboard/cloud
Network My Apps System Sidebar Wallet /dashboard/server
Wallet Cloud nothing Network nothing /dashboard/web5
System Network Quick Start Sidebar nothing /dashboard/settings
Quick Start System nothing Sidebar nothing Drill into [Y]

Quick Start [Y] inner controls

[Y] Open a Shop    [Y] Accept Payments    [Y] File Browser
Position Left Right Escape
Open a Shop nothing Accept Payments Back to [C]
Accept Payments Open a Shop File Browser Back to [C]
File Browser Accept Payments nothing Back to [C]

APPS /dashboard/apps

Nav bar [N]

[N] My Apps  [N] App Store  [N] Services  |  [N] All  [N] Bitcoin  [N] Social (etc)  |  [N] Search

Three groups: page tabs, category filters (dynamic), search input.

Position Up Down Left Right Enter
[N] My Apps nothing App1 nothing App Store Switch tab
[N] App Store nothing App1 My Apps Services /dashboard/discover
[N] Services nothing App1 App Store All filter Switch tab
[N] All nothing App1 Services Bitcoin (etc) Filter
[N] Search nothing App1 last filter nothing Type text

Grid [C] (3-col)

Row 1:  [C] App1    [C] App2    [C] App3
Row 2:  [C] App4    [C] App5    [C] App6
(etc)
Position Up Down Left Right Enter
App1 (row 1) [N] bar My Apps App4 Sidebar App2 Launch app
App2 (row 1) [N] bar My Apps App5 App1 App3 Launch app
App3 (row 1) [N] bar My Apps App6 App2 nothing Launch app
App4 (row 2) App1 App7 Sidebar App5 Launch app
App5 (row 2) App2 App8 App4 App6 Launch app
App6 (row 2) App3 App9 App5 nothing Launch app
(etc) above below left/side right Launch app

App [Y] inner controls (if no launch action)

[Y] Stop    [Y] Restart    [Y] Uninstall

Escape exits back to [C] app card.


CLOUD /dashboard/cloud

No nav bar.

Grid [C] (3-col)

Row 1:  [C] Photos      [C] Music       [C] Documents
Row 2:  [C] Files        [C] Peer1       [C] Peer2 (etc)
Position Up Down Left Right Enter
Photos nothing Files Sidebar Music Open section
Music nothing Peer1 Photos Documents Open section
Documents nothing Peer2 Music nothing Open section
Files Photos nothing Sidebar Peer1 Open section
Peer1 Music nothing Files Peer2 Open peer files
Peer2 Documents nothing Peer1 nothing Open peer files

NETWORK /dashboard/server

No nav bar.

Grid [C]

Row 1:  [C] Quick Actions (full-width, contains Restart/Check Tor/Auto-Sync/Logs)
Row 2:  [C] Local Network       [C] Web3
Row 3:  [C] Network Interfaces  [C] Tor Services
Position Up Down Left Right Enter
Quick Actions nothing Local Network Sidebar nothing Drill into [Y]
Local Network Quick Actions Network Interfaces Sidebar Web3 Drill into [Y]
Web3 Quick Actions Tor Services Local Network nothing Drill into [Y]
Network Interfaces Local Network nothing Sidebar Tor Services Drill into [Y]
Tor Services Web3 nothing Net Interfaces nothing Drill into [Y]

WEB5 /dashboard/web5

No nav bar. Containers from child components stacked vertically + side-by-side.

Grid [C]

Row 1:  [C] Action1  [C] Action2  [C] Action3  [C] Action4  [C] Action5  [C] Action6
Row 2:  [C] Wallet              [C] Domains
Row 3:  [C] Nostr Relays        [C] Node Visibility
Row 4:  [C] Connected Nodes

Standard spatial grid nav. Left from leftmost = Sidebar. Enter = drill into [Y] controls.


DISCOVER /dashboard/discover

Nav bar [N]

[N] My Apps  [N] App Store  [N] Services  |  [N] Discover  [N] Categories...  |  [N] Search

Down from nav bar → first container. Nav bar remembers last-focused tab — Up from cards returns to it.

Grid [C]

Featured (2-col):  [C] Featured1    [C] Featured2
All Apps (3-col):  [C] App1         [C] App2         [C] App3
                   [C] App4         [C] App5         [C] App6
                   (etc)

Cards use same style as My Apps: glass-card transition-all hover:-translate-y-1.

Position Up Down Left Right Enter
[N] tabs nothing Featured1 left tab right tab Switch/filter
Featured1 remembered [N] App1 Sidebar Featured2 View details
App1 Featured1 App4 Sidebar App2 Install / details
(etc) above below left/side right Install / details

MESH /dashboard/mesh

Grid [C]

Left column:   [C] Device Status    [C] Actions    [C] Peers List
Right column:  [C] Chat Panel       [C] Tools (Bitcoin/Dead Man/Map)
Position Up Down Left Right Enter
Device Status nothing Actions Sidebar Chat Panel Drill into [Y]
Actions Device Status Peers Sidebar Chat Panel Drill into [Y] buttons
Peers List Actions nothing Sidebar Chat Panel Drill into peer rows
Chat Panel nothing Tools Device nothing Drill into [Y]
Tools Chat Panel nothing Peers nothing Drill into [Y]

Chat flow: Select a peer/channel (Enter on peer row) → focus auto-jumps to message input → type → Enter sends.


FLEET /dashboard/fleet

Grid [C]

Row 1:  [C] Nodes    [C] Online    [C] Offline    [C] Health
Row 2:  [C] Node1    [C] Node2     [C] Node3      (etc)

Spatial grid nav. Enter = view node details.


SETTINGS /dashboard/settings

Mixed page: Two containers ([C] Server Name, [C] Interface Mode) + linear buttons. Up/Down steps through elements. Right navigates paired items on the same row. Left → sidebar. Enter on containers → drill in. Enter on buttons → activate. Escape → exit container / sidebar.

[C] = Container [B] = Button [I] = Input [T] = Toggle

Account Section (glass-card)

 1. [C] Server Name              → Enter: edit name, Enter: save, Escape: cancel
    [B] What's New               → right of Server Name
 2. [B] Copy DID
 3. [B] Copy Onion Address
 4. [B] Change Password          → opens modal
 5. [B] Enable 2FA / Disable 2FA → opens modal
 6. [B] Logout

System Section

 7. [C] Interface Mode           → Enter: drill in, Left/Right between Easy/Gamer/Chat, Enter: select, Escape: exit
    [B] Language buttons          → below Interface Mode
 8. [B] Login with Claude        → opens modal
 9. [T] Enable All (AI data)     + per-category [T] toggles
10. [B] Manage Updates
11. [I] Webhook URL
12. [I] Webhook Secret
13. [T] Container Crash  [T] Update Available
14. [T] Disk Space Warning  [T] Backup Complete
15. [B] Save Configuration  [B] Send Test
16. [T] Enable Beta Telemetry
17. [B] Create Backup
18. [B] Export Channel Backup
19. [B] Network Diagnostics      → navigates to /dashboard/server
20. [B] Reboot                   → opens confirm modal
21. [B] Factory Reset            → opens confirm modal
Position Up Down Left Right Enter
1. Server Name nothing Copy DID Sidebar What's New Edit name
1b. What's New nothing Copy DID Server Name nothing Show release notes
2. Copy DID Server Name Copy Onion Sidebar nothing Copy to clipboard
3. Copy Onion Copy DID Change PW Sidebar nothing Copy to clipboard
4. Change Password Copy Onion Enable 2FA Sidebar nothing Open modal
5. Enable 2FA Change PW Logout Sidebar nothing Open modal
6. Logout Enable 2FA Language Sidebar nothing Logout
7. Language Logout Claude Login Sidebar nothing Select language
8. Login with Claude Language AI toggles Sidebar nothing Open modal
9. AI toggles (each row) above below Sidebar next toggle Toggle on/off
10. Manage Updates AI toggles Webhook URL Sidebar nothing Open updates
11. Webhook URL Updates Secret Sidebar nothing Edit field
12. Secret Webhook URL Crash toggle Sidebar nothing Edit field
13a. Container Crash Secret Disk Space Sidebar Update Avail Toggle on/off
13b. Update Available Secret Backup Done Container Crash nothing Toggle on/off
14a. Disk Space Warning Crash Save Config Sidebar Backup Done Toggle on/off
14b. Backup Complete Update Avail Send Test Disk Space nothing Toggle on/off
15a. Save Configuration Disk Space Telemetry Sidebar Send Test Save
15b. Send Test Backup Done Telemetry Save Config nothing Send test webhook
16. Telemetry Save/Test Create Bkup Sidebar nothing Toggle on/off
17. Create Backup Telemetry Export Chan Sidebar nothing Open modal
18. Export Channel Create Bkup Net Diag Sidebar nothing Export
19. Network Diagnostics Export Chan Reboot Sidebar nothing → /dashboard/server
20. Reboot Net Diag Factory Rst Sidebar nothing Open confirm
21. Factory Reset Reboot nothing Sidebar nothing Open confirm

LOGIN /login

No sidebar, no grid. Three modes on the same route. [B] = Button [I] = Input field [L] = Link

Set Password (first visit after onboarding)

Auto-focus: [I] Password

[I] Password
[I] Confirm Password
[B] Set Password
[L] Replay Intro    [L] Restart Onboarding
Position Up Down Left Right Enter
[I] Password nothing [I] Confirm nothing nothing Type / Down
[I] Confirm [I] Password [B] Set Password nothing nothing Type / Down
[B] Set Password [I] Confirm [L] Replay Intro nothing nothing Submit
[L] Replay Intro [B] Set Password nothing nothing [L] Restart Replay intro
[L] Restart [B] Set Password nothing [L] Replay Intro nothing Restart onboarding

Normal Login

Auto-focus: [I] Password

[I] Password
[B] Login
[L] Replay Intro    [L] Restart Onboarding
Position Up Down Left Right Enter
[I] Password nothing [B] Login nothing nothing Type / Down
[B] Login [I] Password [L] Replay Intro nothing nothing Submit
[L] Replay Intro [B] Login nothing nothing [L] Restart Replay intro
[L] Restart [B] Login nothing [L] Replay Intro nothing Restart

TOTP Verification (after password accepted)

Auto-focus: [I] TOTP Code

[I] TOTP Code
[B] Verify
[L] Use Backup Code
Position Up Down Left Right Enter
[I] TOTP Code nothing [B] Verify nothing nothing Type / Down
[B] Verify [I] TOTP Code [L] Backup Code nothing nothing Submit
[L] Use Backup Code [B] Verify nothing nothing nothing Toggle backup mode

ONBOARDING /onboarding/*

No sidebar, no grid. Sequential wizard screens. [B] = Button [I] = Input field [C] = Selectable card [L] = Link

Global onboarding rules:

  • No sidebar or nav bar on any onboarding screen.
  • First interactive element auto-focused on each screen (inputs when present, otherwise primary button).
  • B button (Escape) = go back to previous onboarding step (where applicable).
  • D-pad Up/Down always moves between focusable elements — inputs are never trapping. Up/Down exits a focused input to the adjacent element.
  • Enter on an input = submit if it's the last field, otherwise move to next field.
  • Enter activates the focused element.

INTRO /onboarding/intro

Default focus: [B] Unlock

[B] Unlock your sovereignty
[L] Restore from backup
Position Up Down Left Right Enter
[B] Unlock nothing [L] Restore nothing nothing → /onboarding/path
[L] Restore [B] Unlock nothing nothing nothing Show restore panel
[I] File picker
[I] Passphrase
[B] Cancel    [B] Restore
Position Up Down Left Right Enter Escape
[I] File picker nothing [I] Passphrase nothing nothing Open file dialog Close panel
[I] Passphrase [I] File picker [B] Cancel nothing nothing Type / Down Close panel
[B] Cancel [I] Passphrase nothing nothing [B] Restore Close panel Close panel
[B] Restore [I] Passphrase nothing [B] Cancel nothing Submit restore Close panel

PATH /onboarding/path

Default focus: [C] Fresh Start

[C] Fresh Start    [C] Restore (disabled)    [C] Connect (disabled)
[B] Continue
Position Up Down Left Right Enter
[C] Fresh Start nothing [B] Continue nothing [C] Restore Select option
[C] Restore nothing [B] Continue [C] Fresh Start [C] Connect nothing (disabled)
[C] Connect nothing [B] Continue [C] Restore nothing nothing (disabled)
[B] Continue [C] Fresh Start nothing nothing nothing → /login (complete)

OPTIONS /onboarding/options

Default focus: [C] Sovereignty

Row 1:  [C] Sovereignty     [C] Commerce       [C] Projects
Row 2:  [C] Transmitter     [C] Hoster         [C] AI
[B] Continue
Position Up Down Left Right Enter
[C] Sovereignty nothing [C] Transmitter nothing [C] Commerce nothing (display)
[C] Commerce nothing [C] Hoster [C] Sovereignty [C] Projects nothing (display)
[C] Projects nothing [C] AI [C] Commerce nothing nothing (display)
[C] Transmitter [C] Sovereignty [B] Continue nothing [C] Hoster nothing (display)
[C] Hoster [C] Commerce [B] Continue [C] Transmitter [C] AI nothing (display)
[C] AI [C] Projects [B] Continue [C] Hoster nothing nothing (display)
[B] Continue [C] Transmitter nothing nothing nothing → /onboarding/did

DID /onboarding/did

Loading state: No interactive elements. Auto-advances when generation completes.

After generation:

Default focus: [B] Continue

[B] Copy DID
[B] Copy Nostr (if available)
[B] Continue
Position Up Down Left Right Enter
[B] Copy DID nothing [B] Copy Nostr nothing nothing Copy to clipboard
[B] Copy Nostr [B] Copy DID [B] Continue nothing nothing Copy to clipboard
[B] Continue [B] Copy Nostr nothing nothing nothing → /onboarding/identity

If no Nostr ID: [B] Copy DID → Down → [B] Continue directly.


IDENTITY /onboarding/identity

Auto-focus: [I] Name

[I] Identity Name
[C] Personal    [C] Business    [C] Anonymous
[B] Continue
Position Up Down Left Right Enter
[I] Name nothing [C] Personal nothing nothing Type / Down
[C] Personal [I] Name [B] Continue nothing [C] Business Select purpose
[C] Business [I] Name [B] Continue [C] Personal [C] Anonymous Select purpose
[C] Anonymous [I] Name [B] Continue [C] Business nothing Select purpose
[B] Continue [C] Personal nothing nothing nothing → /onboarding/backup

BACKUP /onboarding/backup

Auto-focus: [I] Passphrase

[I] Passphrase
[B] Download Backup
[B] Continue (disabled until downloaded)
Position Up Down Left Right Enter
[I] Passphrase nothing [B] Download nothing nothing Type / Down
[B] Download [I] Passphrase [B] Continue nothing nothing Create & download backup
[B] Continue [B] Download nothing nothing nothing → /onboarding/verify

[B] Continue disabled (skip focus) until backup downloaded.


VERIFY /onboarding/verify

Phase 1 — Signing:

Default focus: [B] Sign Challenge

[B] Sign Challenge
Position Up Down Left Right Enter
[B] Sign Challenge nothing nothing nothing nothing Sign crypto challenge

Phase 2 — After verification:

Default focus: [B] Finish

[B] Finish
Position Up Down Left Right Enter
[B] Finish nothing nothing nothing nothing → /onboarding/done

DONE /onboarding/done

Default focus: [B] Set Password

[C] Identity    [C] Backup    [C] Ready
[B] Set Password
Position Up Down Left Right Enter
[C] Identity nothing [B] Set Password nothing [C] Backup nothing (display)
[C] Backup nothing [B] Set Password [C] Identity [C] Ready nothing (display)
[C] Ready nothing [B] Set Password [C] Backup nothing nothing (display)
[B] Set Password [C] Identity nothing nothing nothing → /login

Onboarding & Login Rules

  1. No sidebar or nav bar — linear wizard flow.
  2. First interactive element auto-focused (input fields when present, otherwise primary button).
  3. D-pad Up/Down always moves between focusable elements — inputs are never trapping. You can always D-pad out of a focused field.
  4. Left/Right for horizontal card rows only.
  5. Disabled elements are skipped in focus order.
  6. B button (Escape) navigates back one onboarding step.
  7. Enter on input: submits if last field, otherwise advances to next field.
  8. No wrap — edges are dead stops.
  9. No dead ends — every screen has a forward action.

Rules

  1. Sidebar: Up/Down wrap. Right → first [C]. Left → nothing.
  2. Grid: arrows move between [C] spatially. No wrap at edges.
  3. Left from leftmost [C] → Sidebar active tab.
  4. Up from top-row [C] → [N] nav bar (if page has one), else nothing.
  5. Enter on [C]: has link → navigate. No link → drill into [Y].
  6. Inside [Y]: arrows move between inner controls. Escape → back to [C].
  7. Escape from [C] → Sidebar.
  8. No dead ends.

Implementation Notes (for future sessions)

Key files

  • Navigation logic: neode-ui/src/composables/useControllerNav.ts
  • Controller store: neode-ui/src/stores/controller.ts
  • Nav sounds: neode-ui/src/composables/useNavSounds.ts
  • Focus styles: neode-ui/src/style.css (lines ~53-142, search focus-visible)

Data attributes

Attribute Purpose
data-controller-zone="main" Main content area (<main> in Dashboard.vue)
data-controller-zone="sidebar" Sidebar nav
data-controller-container + tabindex="0" Focusable card tile — gamepad can land on it, Enter drills in
data-controller-install Container has Install button (Enter prioritizes it)
data-controller-launch Container has Launch button (Enter prioritizes it)
data-controller-install-btn The actual Install button inside a container
data-controller-launch-btn The actual Launch button inside a container
data-controller-ignore Skip element and descendants from gamepad nav
tabindex="-1" Remove from gamepad focus order (used on ToggleSwitch)

Focus memory keys

Key Purpose Cleared on
sidebar Last sidebar item focused never (persists)
main Last container/element in main zone route change
navBar Last nav bar tab (for Up return from containers) route change

Navigation handler order (handleKeyDown)

  1. Text inputs — special handling (Enter submits, Up/Down exits field)
  2. Escape — close overlays → exit inner controls → exit to sidebar → back on detail pages
  3. Enter — container actions (install/launch/link/inner) → regular click
  4. Sidebar — Up/Down wrap, Right → main (containers or first focusable)
  5. Inside container — arrows move between inner controls, can't leave via arrows
  6. Nav bar items — Left/Right between tabs, Down/Up to nearest focusable (containers + buttons)
  7. Main zone — spatial nav through containers + standalone focusables, fallbacks for edges

Mixed pages (containers + standalone buttons, e.g. Settings)

  • isNavBarItem() returns false on container-free pages (lets main zone handler do linear nav)
  • Both nav bar handler and main zone handler search containers + standalone focusables together
  • This prevents "jumping" where Down skips standalone buttons to reach the next container
  • The filter el.hasAttribute('data-controller-container') || !el.closest('[data-controller-container]') excludes inner buttons

Container-free pages (e.g. Settings if all containers removed)

  • Sidebar → Right: checks zone.querySelector('[data-controller-container]') — if none found, focuses first focusable immediately (no 1s poll delay)
  • isNavBarItem() returns false (prevents nav bar handler from catching everything)
  • Main zone handler's spatial nav through all focusables handles Up/Down/Left/Right

ToggleSwitch component

  • Has tabindex="-1" and data-controller-ignore — invisible to gamepad nav
  • Parent button handles the toggle click, so the switch doesn't need its own focus
  • Without this, nav gets stuck bouncing between parent button and toggle switch

Focus glow styles (Chromium gotchas)

  • box-shadow: 0 0 0 Npx (spread-based ring) does NOT follow border-radius on composited layers (translateZ(0))
  • outline doesn't follow border-radius in Chrome < 94
  • Safe approach: use blurred box-shadow (0 0 6px 2px) or border-color change for focus rings
  • All [data-controller-container] have outline: none !important to kill browser defaults
  • Cards use glass-card transition-all hover:-translate-y-1 for consistent hover/focus lift

Mesh chat auto-focus

  • openChat(), openChannelChat(), openArchChannel() all call nextTick(() => chatInputEl.value?.focus())
  • Message input has @keydown.enter.exact.prevent="handleSendMessage" — Enter sends immediately
  • Ref: chatInputEl on the <input> element in Mesh.vue