Skip to content

Subsystem Web App

OneSeventyFour edited this page May 14, 2026 · 1 revision

Subsystem: Web app (Next.js)

The browser-side and server-side both live in host/byh_app/backyardhero/. It's a standard Next.js 14 app using the Pages Router. SSR is unused — the app renders client-side after a quick HTML shell.

Process model

Process Where Purpose
npm run start (production) or npm run dev (dev) inside the container, supervisord program byh-app Serves HTML + JS + the /api/* REST routes on port 1776.

The Next.js server is the only HTTP-facing process. It does not hold any long-lived state — every request is independent. Persistent state lives in SQLite (/data/backyardhero.db) and the daemon's state file (/data/state).

Routing — single-page operator console

The app has effectively one Next.js route: / (src/pages/index.js). Everything else is rendered inside MainNav.jsx as in-memory tabs. URLs do not encode tab; deep-linking goes to whatever tab was last selected.

The seven user-facing tabs:

Key Component Page
main ConsolePanel Console
receivers ReceiverDisplay Receivers
editor ShowBuilder Editor
loadout ShowLoadout Loadout
inventory InventoryManager Inventory
manual ManualFiring Manual fire
setting SettingsPanel Settings

Two are gated:

  • Loadout is hidden until a show is staged.
  • Receivers shows a red error badge if useShowReceiverVerification() reports problems with the staged show.

State stores

Two Zustand stores:

useAppStore.js

The "app data" store. Mirrors most SQLite tables and handles all CRUD against the /api/* REST routes.

Holds:

  • Shows (shows, stagedShow, stagedShowId — the last is persisted to localStorage so a page reload restores your stage). Methods: fetchShows, createShow, updateShow, deleteShow, setStagedShow, hydrateStagedShowFromId.
  • Inventory (inventory). Methods: fetchInventory, CRUD, setItemFiringProfile.
  • System config (systemConfig). Methods: fetchSystemConfig, saveSystemConfig.
  • Receivers (mirror of SQL Receivers table). Methods: fetchReceivers, createReceiver, updateReceiver, reloadReceiversOnDaemon, retryReceiver, fetchReceiverConfig.

stagedShow rehydration is intentional: rather than persisting the full show object to localStorage (which goes stale fast — inventory edits, audio re-uploads, etc.), the store persists only stagedShowId and re-fetches the full row from /api/shows on page load. This avoids the entire class of "I edited the show but the Console still shows the old timeline" bugs.

useStateAppStore.js

The "live daemon state" store. Holds one big stateData object that's a parsed copy of the WS payload (which is a parsed copy of /data/state). All consumers — useAppMode, StatusBar, ReceiverDisplay, ShowControl — read from here.

The store has two setters:

  • setStateData(payload) — replace the whole state (used for a fresh WS message).
  • patchStateData(partial) — merge a partial (used for the WS server's heartbeat that only carries fw_last_update).

How the WebSocket loop works

StatusBar.jsx opens the WebSocket on mount:

const socket = new WebSocket(`ws://${window.location.host.split(':')[0]}:8090`);

(Port 8090 is hard-coded — that's the WS server. The hostname is derived from window.location so it works whether you're on localhost, a Pi at 192.168.1.50, etc.)

On each message:

  • If the payload is { "_hb": true, "fw_last_update": 1715680000123 }, only the fw_last_update is patched in (heartbeat — used to drive the freshness indicators in the status bar).
  • Otherwise, the entire stateData is replaced.

On disconnect, StatusBar does an exponential backoff reconnect.

API routes

All under src/pages/api/. Each one is a thin wrapper around either SQLite (via src/util/sqldb.js) or the daemon command drop-box (/tmp/d_cmd/). See API reference for the full list.

Highlights:

  • /api/system/cmd_daemon — the universal back-channel to the daemon. Any JSON object you POST is written verbatim to /tmp/d_cmd/<timestamp>.json. The daemon polls that directory at 2 Hz, reads the file, deletes it, and routes by type.
  • /api/system/configGET returns systemcfg.json overlaid with the live Receivers table from SQLite (so the UI always sees the canonical receivers). POST writes back the JSON, stripping any receivers block (so the SQL table stays authoritative).
  • /api/system/ota_flashPOST accepts a base64-encoded firmware blob, writes it to /tmp/ota_staging/<filename>, drops { "type": "ota_flash_start", "ident": ..., "image_path": ..., "rate": ... } for the daemon. DELETE drops { "type": "ota_flash_abort" }.
  • /api/inventory/[id]/reprocess-profile — spawns process_firing_profiles.py for one inventory item.
  • /api/catalog/refresh — spawns crawl_catalog.py.
  • /api/inventory/parse-shell-descriptions — calls OpenAI via OPENAI_API_KEY to parse a paste of shell description text into structured shell rows.

Component layout

A simplified file tree of the components directory:

src/components/
├── MainNav.jsx                  -- top-level tab router
├── shell/
│   ├── AppShell.jsx             -- 3-row grid (header / main / footer), armed rail
│   ├── TopBar.jsx               -- header with logo, mode badge, tab list
│   ├── StatusBar.jsx            -- footer; WebSocket consumer; system health
│   ├── useAppMode.js            -- single mode badge derivation
│   └── ModeBadge.jsx
├── console/
│   └── ShowControl.jsx          -- the load/launch/abort flow
├── builder/
│   ├── ShowBuilder.jsx          -- the editor itself (very large)
│   ├── ShowStateHeader.jsx
│   ├── Timeline.jsx             -- main timeline (used in editor and console)
│   ├── AudioWaveform.jsx        -- WaveSurfer-based waveform
│   ├── AddItemModal.jsx
│   ├── ChainTimingModal.jsx
│   ├── CopyItemModal.jsx
│   ├── RacksTab.jsx             -- sub-tab in the editor
│   ├── RackGrid.jsx             -- 2D rack editor
│   ├── RackResizeModal.jsx
│   ├── RackShellsSelector.jsx
│   ├── FuseModal.jsx
│   ├── FusedLineBuilderModal.jsx
│   ├── FusedItemLineBuilderModal.jsx
│   ├── CellShellSelector.jsx
│   ├── ShowTargetGrid.jsx       -- per-show receiver target grid editor
│   ├── ShowReceiverModal.jsx
│   ├── SpatialLayoutMap.jsx     -- Leaflet satellite map
│   ├── SatelliteMap.jsx
│   ├── AddressSearch.jsx        -- Nominatim geocoding
│   └── ItemPreview.jsx
├── receivers/
│   ├── ReceiverDisplay.jsx      -- fleet status grid
│   └── ShowLoadout.jsx          -- packing list / spatial PDF
├── inventory/
│   ├── InventoryManager.jsx
│   ├── InventoryList.jsx
│   ├── ImportCatalogModal.jsx
│   ├── ShellPackEditor.jsx
│   └── ShotProfileModal.jsx     -- firing profile editor (YouTube)
├── settings/
│   ├── SettingsPanel.jsx        -- top-level tabs
│   ├── BrightnessSlider.jsx
│   ├── DebugModeToggle.jsx
│   ├── TxConfig.jsx
│   ├── ReceiverConfigSettings.jsx
│   ├── DaemonSettings.jsx
│   ├── RFScanPanel.jsx
│   ├── OtaFlashPanel.jsx
│   ├── ProtocolConfig.jsx
│   └── DefaultLocationSettings.jsx
├── manual/
│   ├── ManualFiring.jsx
│   └── ManualFirePanel.jsx
└── debug/                       -- internal/dev-only diagnostics

Other utilities of note

  • src/util/sqldb.js — single source of truth for the SQLite schema. Initializes tables on every Next.js boot and runs ALTER TABLE ADD COLUMN migrations idempotently. See Subsystem: Database.
  • src/util/showReceivers.js — helpers for the per-show receiver list (the show_receivers JSON column). availableDevicesFromShowReceivers, verifyShowReceivers, summarizeVerificationErrors.
  • src/util/useShowReceiverVerification.js — combines the staged show with the global receivers table to produce a list of verification errors (missing receiver, disabled receiver, not enough cues).

Styling

  • Tailwind for layout and one-off styles. Configuration in tailwind.config.js.
  • A custom dark-only design system in src/styles/globals.css exposing CSS variables (--bg-elev, --fg, --accent, --ok, --warn, --danger, plus --armed-channel / --live-channel for the operational rails).
  • Mode hooks: html.mode-armed, html.mode-live, html.mode-disconnected shift the accent / foreground tokens. The "armed rail" — the angled barber-pole indicator on the side of the UI when armed/live — is a single SVG repeating-gradient driven by these classes.

Build

The Dockerfile runs npm ci && npm run build so the production image ships a pre-built Next.js app. Dev mode replaces this with npm run dev for HMR; the supervisord config (supervisord.dev.conf) handles that.

Clone this wiki locally