R2-D2
Dashboard
Node Red
Restreaming
The Force
UpdatedNever
v1.0.0

Overview

Loading…
R2-D2
Dashboard
Node Red
Restreaming
The Force
UpdatedNever
v1.0.0
DDroidspeak / Docs
Operator handbook
Droidspeak
What is R2-D2?
Runtime architectureAuth — Keycloak migration plan
FleetRestreamingGalaxy MapThe RepublicTemple ArchivesSTANAG 4817The Force
Tech StackNext.js 15 + React 19Tailwind v4ZustandTanStack Table + DataViewhls.jsLow-latency playerreagraphoglreact-grid-layoutMonacoScalar API Referencefumadocs
Cluster InfraTrailBaseReductStoreRestreamer (datarhei/core)TBMQKeycloakLonghornkube-vipIngress (Caddy + nginx-ingress + Traefik)Netbird
Operator QA Runbook
Install — RKE2Install — Dokploy
Contributor guideRelease Notes
Tech Stack

Next.js 15 + React 19

App Router, RSC vs client, dev-server invariants, and where state actually lives.

The dashboard is a single Next.js 15 app running React 19. Every page is in the App Router. There is one Next process per environment; there is no separate API tier. Route handlers under src/app/api/* are the dashboard's only server surface.

Version pins

PackagePinWhy
next^15.2.0App Router + RSC stable; Turbopack opt-in but not required
react, react-dom^19.0.0RSC, useActionState, useOptimistic — used in mutation flows
typescript^5.7.0Required for the React 19 type updates
@types/react^19.0.0Forced bump from 18 — caused the Vidstack-stable pivot (see Low-latency player)

React 19 types break some libraries pinned to 18. If you add a third-party React component package, check its peer-deps before installing. The known fallout so far is @vidstack/react stable, which is why the public viewer uses a custom chrome wrapper instead.

App Router layout

Top-level route segments under src/app/:

SegmentPlanePurpose
/n/aRoot dashboard — fleet overview
instances/FleetNode-RED instance management
flows/FleetPer-instance flow visualizer + grid + table
restreaming/RestreamingHoloVids grid, Holoprojector, layouts, panels, groups
galaxy-map/Galaxy MapGeographic site/link/status renderer
the-republic/RepublicNations + holdings
temple-archives/Temple ArchivesConfiguration, farsight-feeds, feed-sync, vessel-categorization
the-force/The ForceSense (k8s) + embedded external apps
droidspeak/DocsThis handbook (fumadocs)
registry/SettingsYAML registry editor
nodered/, data/, deploy/, settings/, login/variousSmaller operator surfaces
api/serverAll route handlers (see Route handlers below)

Layout files at each level set the chrome (Sidebar, ToastContainer, SoundsProvider). The root layout.tsx mounts the global providers; nested layouts only add per-plane chrome (e.g. restreaming/layout.tsx mounts the holovids/holoprojector sub-nav).

RSC vs client components

The split is opinionated:

Lives on the server (RSC)Lives in the browser ("use client")
Every list page that maps over an array (instances, feeds, layouts)Anything that uses a hook (useState, useEffect, zustand)
Initial data fetch (calls a route handler or reads YAML directly)Forms, drag-and-drop, video, WebGL, Monaco
Read of holocron-config.yaml, archives-config.yaml, republic-config.yamlLive status overlays (zustand subscribers)
Auth-gated wrappers that decide whether to renderSidebar (uses useViewMode + sound provider)

The client/server boundary is drawn at the smallest component that needs the browser. app/restreaming/holovids/page.tsx is a server component that fetches the feed list and passes it into a client <HoloVidsGrid> child — the page itself stays serializable.

Route handlers

Every server endpoint is a route handler under src/app/api/*. There is no Pages-API fallback and no Edge runtime — everything is Node runtime by default.

Key route families:

Route familyBacked byPages that consume it
/api/instances/*instances/registry.yaml + live probeFleet, Galaxy Map
/api/restreaming/*Restreamer (datarhei/core) + TrailBase + ReductStoreHoloVids, Holoprojector, Galaxy Map
/api/restreaming/snapshots/*ReductStore (snapshot history bucket)HoloVids tiles, Snapshot history tab
/api/republic/*TrailBase + republic-config.yamlRepublic
/api/registry/*YAML files in repoRegistry editor
/api/nodered/*Per-instance Node-RED admin APIFlows

Three rules every route handler obeys:

  1. Read-before-write on every mutation. Pull current state, diff in-memory, PUT the merged result. Without this, two operators editing overlapping fields corrupt the upstream config mid-flight.
  2. Per-item results on bulk routes. Bulk create/delete/command always returns [{ id, ok, error? }] — never a single boolean.
  3. Audit on every mutating call. One JSONL line to data/<plane>-audit.jsonl per POST/PUT/DELETE.

State on the client

Three layers, in order of preference:

LayerWhen to useExamples
URLAnything an operator might link to or refresh intoCurrent panel, current group, active filter
zustandLive status feeds shared across pagesFleet health, restreaming health, sound mute
localStorageDisplay preferences only (never source of truth)Grid/list toggle, HUD slot selection, sidebar collapse

See zustand for the full store inventory.

The dev-server invariant

R2-D2 has one production-grade local dev server, and it lives on port 3210:

npm run dev:persist        # starts/keeps next dev on :3210
npm run dev:persist:clean  # nukes .next first

Never run next dev directly from an IDE background shell, and never run two dev servers on 3210 — both paths corrupt .next and the symptoms (route handlers 500ing randomly, RSC payloads dropping) take longer to debug than they're worth.

If you see local 500 / Internal Server Error on port 3210 while developing, dev:persist:clean is the first thing to try.

Build vs dev output isolation

The build path (npm run build) writes into the same .next/ as next dev. Without isolation, a build kicked off while dev is running occasionally corrupts the running server's chunk graph. The workaround until output isolation lands: stop dev:persist before running build.

Extension points

  • Adding a route handler — drop a route.ts under src/app/api/<segment>/ with a GET/POST/PUT/DELETE named export. The middleware path-prefix matching picks it up automatically.
  • Adding a plane — create src/app/<plane>/page.tsx (server) plus an entry in src/lib/nav-config.yaml. Add a stack page here and a components page in Components.
  • Adding a client widget — put it in src/components/<plane>/ or src/components/ (shared). Mark only the leaf component with "use client" so the page tree stays serializable.

See also

  • Tailwind v4 — design tokens used by every page
  • Zustand — where shared state lives
  • TanStack Table — the only way to render lists
  • Architecture — system-level diagram

Tech Stack

One page per technology — what it does in this dashboard, where to find it, and what it constrains.

Tailwind v4

Design tokens, the HUD palette, and the dark-only design language.

On this page

Version pinsApp Router layoutRSC vs client componentsRoute handlersState on the clientThe dev-server invariantBuild vs dev output isolationExtension pointsSee also