Restructure the workshop flow into the designer's cockpit: a persistent shell
(header + 5-step stepper + sticky instrument rail) wrapping the phase routes via
a React-Router layout route, so the rail stays mounted across navigation.
- Design system: IBM Plex Mono + Newsreader; the full cockpit token set (light
+ dark) in index.css; a working light/dark theme toggle (store `theme` +
useApplyTheme); a `switch` ui primitive.
- Shell: CockpitLayout, Stepper (forward-gated), PanelChrome helpers. Every
phase page restyled to the editorial panels + the WORKSHOP-FLOW fixes
(channels-after-bind, domain framing + L1 prefill, L2/L3 prefill at 3/3,
in-place submission finale). Store gains `tried` + `channels.saidHi` (v4).
- Live rail (CockpitRail): real node heartbeat + agent activity log + ADD
progress; sim telemetry (useTelemetry) for the waveform/accel/I2C behind a
seam, marked SIM.
- LED-matrix PIXEL MIRROR (real): the rail shows exactly what the physical
matrix displays — API GET /nodes/:team/matrix reads the board's framebuffer
off the :9999 relay (readMatrixFrame + the `matrixget` relay command);
useMatrixMirror polls it and unpacks the 104 bits.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The Zustand store persisted to sessionStorage, which is tab-volatile — a
hard reload / new tab / closing the tab dropped the whole session,
including the board binding (device.connected, nodeUrl, teamId). The
workshop runs on the team's own laptop, so switch the store (and the
offline outbox) to localStorage: the binding + progress now survive a
refresh, hard reload, navigation, and tab close.
Clearing is now explicit only: a new `disconnect()` action drops the board
binding (keeping team/domain/phases/ADD so they can re-bind and resume),
surfaced as a "Disconnect" button on the claimed-board card. `reset()`
still wipes everything.
Tests: add an in-memory localStorage to setupTests (Node 22's disabled
experimental localStorage shadows jsdom's) + clear it per test.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Reframes the app for the local single-team demo and reworks onboarding.
Landing (local-demo chrome):
- Drop Lecture / staff sign-in / programme / stat tiles / bottom CTA.
- Hero → "an APESS agent on the edge"; CTA → "Start the workshop".
- Presenter keeps redclaw.dev + [email protected] contact.
Phase strip:
- Colour by position off the active phase (earlier=done, current=active,
later=pending) so a stale/persisted flag can't light up a future phase
green (fixes "phase 2 green while on phase 1").
Phase 1 (Team registration) — consolidated onboarding:
- After the board connects, reveal Say hi (moved from Phase 2), Extra
channels (Telegram wizard), and a Voice enable/disable toggle.
- Phase 2 "Meet your node" thins to Open-your-node + Pick-your-domain;
removed the Lock-it-down card. New session `channels {telegram, voice}`
(persist v3, non-destructive migration).
Live Telegram apply (browser → APESS API → board ZeroClaw gateway):
- Wizard Finish pushes the bot token to the running node and restarts it:
PUT /api/config/prop channels.telegram.main.bot_token (gateway
auto-creates the alias + enc2-encrypts the secret) then POST
/admin/reload (in-place subsystem reload → the Telegram channel starts).
- api: bridge configureTelegram + POST /nodes/:teamId/telegram; client
configureTelegram(); wizard shows applying/error and only saves on
success. Remote /admin/reload needs the board's gateway.allow_remote_admin
(+ pairing); on a fully-open board the reload step 403s.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The board is now a Claude-powered ZeroClaw agent with expert skills that teams
talk to directly (its own dashboard / Telegram / voice). The frontend was built
on the older sensor+harness model; this repoints it.
- State/DTO: AddLayers remapped to the 5 design layers (Domain/Skills/Policies/
Harness/Loops); added session.domain (+ mirrored through TeamSnapshot, sync,
and the SQLite store with a guarded migration); dropped Harness/Provider/RunMode;
persist v2 migrate resets stale state.
- Removed the harness/sim/failover machinery (HarnessProviderSelect, HarnessTuner,
HarnessTomlPreview, ResiliencePanel, TriggerButtons, harness.ts, useSerial,
LiveFeed/serial classifier) and every sim-vs-live branch.
- Onboarding: EnvSetup rebuilt into 'Meet your node' — Open-your-node hero (new
reusable OpenYourNode CTA), say-hi-to-your-agent, a free-text DomainPicker
(domain drives L1-L5), Telegram/voice pointers, and the open->locked lockdown
policy step. Domain is required to proceed.
- Modules repointed: M1 Domain & events (L1), M2 Skills & Policies (L2/L3, skills
reference + actuation-gate framing), M3 Harness (reasoning+tiering) & Loops
(cadence) (L4/L5). BuildFlash reframed as a guided 'ask your node to build X'
that hands off to the node dashboard.
- Judge rubric -> Domain fit/Skills/Policies/Harness/Loops; Lecture + Landing
re-storied to the Claude-node + talk-to-your-node narrative.
Verified: web tsc + 181 tests, api tsc + 58 tests, dead-ref sweep clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The board self-registers its gateway URL (http://<lan-ip>:8080) but the API
withheld it from the browser — teams saw only an online boolean. The bearer
token is the only real secret; the URL is safe to expose. Return it in the
/claim response and the node status poll, thread it into device.nodeUrl, and
render an 'Open your node →' deep-link in EnvSetup's connected-device card so a
team can reach its own node's embedded web chat directly on the workshop LAN.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fixes a latent bug and hardens claim recovery. Refactors the unclaimed pool
into a BoardRegistry that tracks claimedBy, so a board's binding survives its
own reboot and a team can get back onto its board after an error.
- Auto-heal: a claimed board that reboots re-announces with a fresh IP/token;
self-register now REFRESHES its node binding instead of dumping it back into
the unclaimed pool (previously the team's board went stale/offline + the kit
wrongly reappeared as unclaimed).
- Resume: re-claiming an already-claimed kit with the right code returns the
board's CANONICAL teamId + team snapshot (not a fresh identity). The client
adopts it via a new resumeTeam() action, restoring name/members/phases/stats
so a lost-browser re-claim doesn't clobber synced progress.
- Release: POST /claim/release (admin) frees a kit back to the pool + unbinds
its node, for mis-claims / reassignment. Wired to a small control in /admin.
API 56 tests green; web 220 green; tsc + eslint clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Turn the workshop shell into a real on-device agent platform: APESS now
proxies each team's Arduino Uno Q (running ZeroClaw) and drives
generate → compile → flash from the UI, with a simulation fallback.
api — node backbone
- nodes.ts: team→node registry, a pure ZeroClaw /api/events → WsEvent
mapper, per-node live SSE subscription fanned out to the collective hub
AND per-team participant listeners, and a prompt proxy. Bearer tokens
stay server-side; list()/broadcasts never expose them.
- routes: POST/GET/DELETE /nodes (register is admin-only), public
POST /nodes/:teamId/prompt, and a participant-scoped SSE
GET /nodes/:teamId/events.
- WsEvent gains node:status / node:activity.
front-end
- session store: mode 'live' | 'sim' (default sim); a 'local' provider
and a harness.fallbackLocal toggle ("cloud first, on-board Qwen if it
fails") reflected in the harness.toml preview.
- EnvSetup: Simulation / Live board toggle — sim runs a virtual board
(self-test + Proceed with no hardware), live requires a registered node.
- BuildFlash panel (in Module 2): prompt the board, watch activity stream
live (SSE) or a simulated generate→flash sequence.
- lib/api: sendPrompt + openTeamActivity (EventSource).
housekeeping
- better-sqlite3 → 12.11 (Node 26 support; the 11.x native build fails on
Node 26's V8).
- vitest excludes the vendored Uno-QClaw/ tree.
Tests: front-end 172, api 29, typecheck clean, prod build passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- session store gains a persisted teamId (crypto.randomUUID, preserved
across reset) used as the team's stable identity for the collective
- src/types.ts: TeamSnapshot/SubmissionDTO/ScoreDTO/LeaderboardRow/WsEvent,
the contract shared with the api backend
2 new tests; suite 88/88 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>