docs(claude): add nested CLAUDE.md files
Init-deep pass: contextual guidance auto-loaded when Claude reads
files in those directories. Closest-CLAUDE.md-wins precedence.
dashboard/ backend wiring (BOX-3 WS, voice-bridge REST, hints SSE)
packages/ workspace scope, deps direction, NodeNext ESM rule
+scenario-engine/ pure TS interpreter, no React/DOM
+shared/ types contract, single js-yaml runtime dep
+ui/ headless+tokens, promotion-bar policy
hardware/ KiCad/PIO satellites, boundary with submodule master
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
# Dashboard
|
||||
|
||||
Live game-master view : connection setup → expert/simple dashboards → real-time puzzle timeline + NPC / voice / hints panels. Sister app to `apps/atelier/`. Same React 19 + Vite 6 + zustand 5 stack, no Blockly / no R3F.
|
||||
|
||||
## Backend wiring
|
||||
|
||||
The dashboard talks to **three independent backends**, each through one hook + a typed contract in `@zacus/shared`. None of them touch each other ; the dashboard is the aggregator.
|
||||
|
||||
| Backend | Transport | Default URL | Hook | Endpoints used |
|
||||
|---------|-----------|-------------|------|----------------|
|
||||
| BOX-3 ESP32 master | **WebSocket** | `ws://zacus-box3.local:81` | `gameStore.connect()` | bidirectional event stream + command frames |
|
||||
| voice-bridge (MacStudio) | **REST polling** | `http://100.116.92.12:8200` | `useVoiceBridge` (2 s) + `useVoiceUsage` (5 s) | `GET /health/ready`, `GET /tts/cache/stats`, `GET /usage/stats`, `POST /usage/reset` (X-Admin-Key) |
|
||||
| hints engine | **SSE preferred, REST polling fallback** | `http://localhost:8311` | `useHintsEngine` | `GET /hints/sessions`, `GET /hints/events` (SSE), `DELETE /hints/sessions/{id}` |
|
||||
|
||||
All URLs are overridable via `VITE_*` env vars (`VITE_VOICE_BRIDGE_URL`, `VITE_HINTS_BASE_URL`, etc.) — the constants live in `packages/shared/src/constants.ts` and are the source of truth.
|
||||
|
||||
**Mock for offline dev** : `pnpm mock` boots `mock/ws-server.ts` on `:81` and replays a deterministic BOX-3 event sequence. Use it for any UI work that does not require a real ESP32 — the voice-bridge and hints backends do not have mocks ; point at the live MacStudio or run the Python services locally.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
src/
|
||||
main.tsx # ReactDOM root
|
||||
App.tsx # Routes ConnectionSetup ↔ ExpertDashboard / SimpleDashboard
|
||||
components/
|
||||
ConnectionSetup.tsx # Host / port form, persists in localStorage
|
||||
ExpertDashboard.tsx # Full grid: Timeline + ControlPanel + all panels
|
||||
SimpleDashboard.tsx # Stripped GM view for non-tech operators
|
||||
Timeline.tsx # Puzzle progression + event log
|
||||
ControlPanel.tsx # Manual triggers (skip, reset, force-hint)
|
||||
PuzzleCard.tsx # Per-puzzle status tile
|
||||
NpcPanel.tsx # Live NPC mood + last utterance
|
||||
VoiceActivityPanel.tsx # Mic state, last STT transcript
|
||||
VoiceUsagePanel.tsx # Cost-audit panel — uses useVoiceUsage hook
|
||||
HintsAdaptivePanel.tsx # Hints engine state + adaptive level
|
||||
hooks/
|
||||
useVoiceBridge.ts # REST polling of voice-bridge :8200 health + cache
|
||||
useVoiceUsage.ts # REST polling of voice-bridge /usage/* (cost audit)
|
||||
useHintsEngine.ts # SSE+polling against the hints engine (auto-fallback)
|
||||
store/
|
||||
gameStore.ts # Zustand : BOX-3 WebSocket + mirrored game state
|
||||
mock/
|
||||
ws-server.ts # `pnpm mock` — fake BOX-3 WS on :81 for offline dev
|
||||
```
|
||||
|
||||
## Patterns
|
||||
|
||||
- **One store** (`gameStore`) — owns the BOX-3 WebSocket, mirrors phase / puzzles / NPC / events. Auto-reconnects via `BOX3_WS_RECONNECT_MS`. The store keeps the last 200 events (`events.slice(-200)`). Don't split per-panel ; the dashboard is a coherent view of one game session.
|
||||
- **Hooks own their endpoints**. Components consume hook output, never `fetch` directly. The three hooks are independent — none import each other.
|
||||
- **`useHintsEngine` defaults to SSE** (`auto` mode) and demotes to polling after a 2 s connect timeout or 3 consecutive `onerror` callbacks. A 30 s heartbeat watchdog reconnects wedged streams. In `jsdom` / Node tests (no `EventSource`), it transparently falls back to polling.
|
||||
- **Test files colocate** : `*.test.tsx` next to the component / hook. Vitest is configured in `vite.config.ts`.
|
||||
- **Mock server is canonical for dev** : run `pnpm mock` instead of pointing at a real ESP32 — replays a fixed sequence of `game_start`, `profile_detected`, `puzzle_solved`, `hint_given`, `npc_spoke` events plus `timer_update` every 2 s.
|
||||
- **Dashboard runs on `:5174`** (atelier uses `:5173`). Both can run together during integration work.
|
||||
|
||||
## Backend gotchas
|
||||
|
||||
- **voice-bridge `:8200`** lives on the MacStudio (`100.116.92.12`). Reachable from any machine on the Tailnet ; from outside, tunnel via `electron-server`. `/health/ready` returns `503` during F5 warmup with a coherent body — the hook treats that as "still loading", not an error.
|
||||
- **`POST /usage/reset`** requires `X-Admin-Key` whenever the bridge env sets `VOICE_BRIDGE_ADMIN_KEY`. Pass it through `VITE_VOICE_BRIDGE_ADMIN_KEY` or `useVoiceUsage({ adminKey })`.
|
||||
- **Hints engine is `:8311`** (per `HINTS_DEFAULT_BASE_URL`). The docstring at `tools/hints/server.py:48` recommends `--port 8300` — **don't follow it**, that port is taken by `whisper.cpp` on the MacStudio. Run with `--port 8311`. The service is typically launched on the GM laptop alongside the dashboard, not on the MacStudio.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Reaching into `apps/atelier/` — these apps share `packages/shared` and `packages/ui` only, never each other's internals.
|
||||
- Fetching from components — go through a hook so mock / real switching stays in one place.
|
||||
- Storing per-component UI state in `gameStore` — keep it `useState`. The store is for game state, not panel toggles.
|
||||
- Opening a WebSocket to voice-bridge `:8200` from here — the WS at `/voice/ws` is reserved for the firmware (per `useVoiceBridge` docstring). Use the REST polls.
|
||||
- Hardcoding hosts / ports — read from `packages/shared/src/constants.ts` (`BOX3_MDNS_HOST`, `VOICE_BRIDGE_DEFAULT_BASE_URL`, `HINTS_DEFAULT_BASE_URL`) or the matching `VITE_*` env.
|
||||
- Treating SSE payloads as data in `useHintsEngine` — they are *refresh signals*. Authoritative state lives behind `GET /hints/sessions`.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Frontend Packages
|
||||
|
||||
Shared workspace packages consumed by `apps/atelier` and `apps/dashboard` via `workspace:*`. Three packages, each a distinct concern.
|
||||
|
||||
```
|
||||
packages/
|
||||
scenario-engine/ # Pure TS scenario interpreter (no React) — engine + profiler + scorer
|
||||
shared/ # Types, constants, yaml-parser — zero runtime deps beyond yaml
|
||||
ui/ # React component library + Tailwind tokens (tokens.css)
|
||||
```
|
||||
|
||||
## Scope rules
|
||||
|
||||
- **`scenario-engine/`** — no React, no DOM. Pure functions over the Runtime 3 IR. Atelier instantiates it for live simulation ; dashboard does not import it. Tests live in `__tests__/`.
|
||||
- **`shared/`** — `types.ts` is the source of truth for IR / event / WS message shapes. `yaml-parser.ts` is the only place that parses scenario YAML on the web side (the canonical compiler is `tools/scenario/compile_runtime3.py`). If a type is used by both apps, it lives here.
|
||||
- **`ui/`** — design-system components only (buttons, panels, layout primitives). App-specific composite components stay in the app. `tokens.css` defines the CSS variables ; `tailwind.config.ts` reads them.
|
||||
|
||||
## Build
|
||||
|
||||
Each package builds independently :
|
||||
|
||||
```bash
|
||||
pnpm --filter @zacus/scenario-engine build
|
||||
pnpm --filter @zacus/shared build
|
||||
pnpm --filter @zacus/ui build
|
||||
```
|
||||
|
||||
TS project references wire it together (`tsconfig.json` in each package + root `tsconfig.json`).
|
||||
|
||||
## Patterns
|
||||
|
||||
- **Re-export from `index.ts`** — apps import `@zacus/shared`, never `@zacus/shared/src/types`.
|
||||
- **Bump together** : a breaking change in `shared` requires touching both apps in the same PR. Keep the public API small to make this cheap.
|
||||
- **No app code in packages** — if it references atelier / dashboard concepts, it stays in the app.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Adding React to `scenario-engine` — it must run in a Node test harness without DOM.
|
||||
- Putting Tailwind classes in `shared` — `shared` has no view layer.
|
||||
- Duplicating YAML parsing logic in an app — extend `yaml-parser.ts` instead.
|
||||
- Cross-importing between packages beyond `shared → (none)`, `ui → shared`, `scenario-engine → shared`. No cycles.
|
||||
@@ -0,0 +1,33 @@
|
||||
# scenario-engine
|
||||
|
||||
TS implementation of the Runtime 3 IR — interpreter + group profiler + scorer. **Pure logic, no React, no DOM**, runs in Node test harness.
|
||||
|
||||
Public API (`src/index.ts`) :
|
||||
|
||||
```ts
|
||||
ZacusScenarioEngine // engine.ts — IR executor
|
||||
detectGroupProfile, profileConfidence // profiler.ts — player group classification
|
||||
computeScore, assembleCode // scorer.ts — end-of-game scoring
|
||||
ScenarioEngine // type re-export from @zacus/shared
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- `src/engine.ts` — the executor. Holds run state, advances pivots, emits events.
|
||||
- `src/profiler.ts` — analyses player behaviour to pick a group profile (drives adaptive hints + NPC tone).
|
||||
- `src/scorer.ts` — final score + the assembled-code reveal.
|
||||
- `__tests__/engine.test.ts` — Vitest, runs without DOM.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Contract = `specs/ZACUS_RUNTIME_3_SPEC.md`** (repo root). The Python compiler `tools/scenario/compile_runtime3.py` produces the IR ; this package consumes it. Both must stay in lockstep — if you add an opcode here, add it to the spec and the Python compiler in the same PR.
|
||||
- **Imports end in `.js`** (NodeNext ESM resolution) even though source is `.ts`. The `index.ts` already follows this.
|
||||
- **Only dep is `@zacus/shared`** for types. Adding another runtime dep needs justification — this package is meant to be embedded in test harnesses and possibly server contexts.
|
||||
- **No `Date.now()`, no `Math.random()`** in `engine.ts` without injection — tests must be deterministic. Pass a clock / RNG through the engine constructor.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Importing React / DOM / browser APIs.
|
||||
- Forking the IR shape locally instead of extending `@zacus/shared/types`.
|
||||
- Calling out to network / fs from the engine — it's a pure interpreter.
|
||||
- Letting `profiler` and `scorer` reach into engine internals — they consume the same IR + event log the host gives them.
|
||||
@@ -0,0 +1,25 @@
|
||||
# shared
|
||||
|
||||
Source of truth for cross-app types, constants, and scenario YAML parsing. **No view layer, no React**. Runtime deps : `js-yaml` only.
|
||||
|
||||
```
|
||||
src/
|
||||
types.ts # IR shapes, event shapes, WS message shapes, ScenarioEngine interface
|
||||
constants.ts # Shared magic numbers / enums
|
||||
yaml-parser.ts # YAML → typed scenario object
|
||||
index.ts # `export * from` each of the above
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- **`types.ts` is the contract**. Both apps and `scenario-engine` import from here. A type change is a workspace-wide change : bump together, check both apps compile.
|
||||
- **`yaml-parser.ts` is the only web-side YAML reader**. The canonical compiler is the Python one in `tools/scenario/compile_runtime3.py` — this parser exists so the atelier can preview / live-diff without a Python round-trip. If they diverge, the Python compiler wins ; mirror the change here.
|
||||
- **Constants stay tiny** — if a value is used in one place, inline it. Promote to `constants.ts` on the second use.
|
||||
- **`.js` import suffix** in TS sources (NodeNext ESM). The existing `index.ts` follows this.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Adding a runtime dep beyond `js-yaml` — the value of `shared` is that it's cheap to embed everywhere.
|
||||
- Re-exporting types under a renamed alias — keep one canonical name.
|
||||
- App-specific types leaking in (e.g. dashboard panel props) — those stay in the app.
|
||||
- Parsing YAML in an app component — go through `yaml-parser` so format changes have one place to land.
|
||||
@@ -0,0 +1,33 @@
|
||||
# ui
|
||||
|
||||
Shared React component library + design tokens. Current surface : `Button`, `Card`, `Badge`, `Progress`. Built on Radix primitives (`@radix-ui/react-{dialog,progress,toggle,tooltip}`), styled via Tailwind + `tokens.css`.
|
||||
|
||||
```
|
||||
src/
|
||||
components/ # Button, Card, Badge, Progress (one file per component)
|
||||
tokens.css # CSS variables — exported as @zacus/ui/styles.css
|
||||
index.ts # Re-exports component + props type per component
|
||||
tailwind.config.ts
|
||||
```
|
||||
|
||||
Consumer imports :
|
||||
|
||||
```ts
|
||||
import { Button, type ButtonProps } from '@zacus/ui';
|
||||
import '@zacus/ui/styles.css'; // tokens.css
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- **Headless + tokens** : Radix gives behaviour, `tokens.css` gives visual identity. Don't hardcode colors in components — reference CSS variables.
|
||||
- **Promotion bar is high** : a component lives in the app until ≥2 apps need it. Premature `ui` entries become churn.
|
||||
- **Props named `{Name}Props` and exported** alongside the component — atelier and dashboard rely on these to type their wrappers.
|
||||
- **Tailwind is allowed here** (unlike `apps/atelier` which is inline-styles / `main.css`). The `tailwind.config.ts` lives next to the source.
|
||||
- **No app state, no zustand, no fetch** — components are stateless or use local `useState` only.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Adding a feature-specific component (e.g. `<NpcMoodBadge/>`) — that belongs in the app that owns the concept.
|
||||
- Importing from `@zacus/scenario-engine` — `ui` doesn't know about scenarios.
|
||||
- Bundling icons / fonts — leave asset choice to the consumer.
|
||||
- Breaking the `ButtonProps`-style export pattern — downstream code does `extends ButtonProps`.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Hardware
|
||||
|
||||
Physical artefacts of the escape room : firmware sources, PCB projects, enclosures, BOM, wiring diagrams, puzzle assembly. The ESP32-S3 master firmware lives in the top-level `ESP32_ZACUS/` submodule — **this folder is the surrounding hardware**, not the master firmware.
|
||||
|
||||
```
|
||||
hardware/
|
||||
firmware/ # Arduino / PlatformIO sketches for puzzle satellites + UI boards
|
||||
projects/ # KiCad projects — plip-telephone, slic-phone, slic-phone-esp32
|
||||
puzzles/ # Per-puzzle BOM, GPIO mapping, assembly checklists, suitcase layout
|
||||
bom/ # Aggregated bill of materials (bom.md)
|
||||
wiring/ # Wiring diagrams
|
||||
enclosure/ # 3D-printed / laser-cut enclosure files
|
||||
ui_freenove_allinone/ # Reference Freenove board UI assets
|
||||
```
|
||||
|
||||
## Where to look
|
||||
|
||||
| Task | Location |
|
||||
|------|----------|
|
||||
| Order parts | `bom/bom.md` + per-puzzle BOMs in `puzzles/BOM_V3_complete.csv` |
|
||||
| Wire a puzzle station | `puzzles/gpio_mapping.md` + `wiring/` |
|
||||
| Build a satellite firmware | `firmware/` (PlatformIO — `platformio.ini`) |
|
||||
| Edit a KiCad schematic / PCB | `projects/<board>/` |
|
||||
| Lay out the suitcase | `puzzles/suitcase_layout.md` |
|
||||
| Print an enclosure | `enclosure/` |
|
||||
|
||||
## Patterns
|
||||
|
||||
- **PlatformIO is the firmware build system here**, not Arduino IDE. Each board has an env in `firmware/platformio.ini`. Override locally with `platformio_override.ini` (gitignored on field machines).
|
||||
- **KiCad projects use the standard kicad skill** — see `/Users/electron/CLAUDE.md` for the kicad-pro MCP server. Don't hand-edit `.kicad_pcb` for routing.
|
||||
- **BOM stays in sync with KiCad symbols** — use the `bom` skill / `lcsc` / `digikey` skills, not manual CSV edits.
|
||||
- **Per-puzzle docs are the source of truth for assembly** — when GPIO assignment changes, update `puzzles/gpio_mapping.md` *and* the firmware constants in the same commit.
|
||||
|
||||
## Boundary with `ESP32_ZACUS/`
|
||||
|
||||
- `ESP32_ZACUS/` (submodule) = the **master** ESP32-S3 (NPC engine, voice pipeline, vision, media manager). Owns the game loop.
|
||||
- `hardware/firmware/` = **satellite** boards (puzzle stations, secondary UIs, audio kits). They speak to the master over the protocol in `firmware/protocol/`.
|
||||
- `PLIP_FIRMWARE/` (top-level) = the retro telephone annex. Distinct from `hardware/projects/plip-telephone` (which is the KiCad project for the same artefact).
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Editing `ESP32_ZACUS/` files from here — it's a submodule with its own repo and CLAUDE.md.
|
||||
- Duplicating BOM rows between `bom/bom.md` and `puzzles/BOM_V3_complete.csv` without marking one as derived.
|
||||
- Changing GPIO assignments without grepping firmware *and* `puzzles/gpio_mapping.md`.
|
||||
- Committing `.pio/`, `build/`, or local-override `platformio_override.ini`.
|
||||
Reference in New Issue
Block a user