Commit Graph
51 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 4.8 af9cae0abc feat(onboarding): surface board LAN URL so teams open their node's ZeroClaw web UI
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]>
2026-07-15 16:26:15 -07:00
Omar SobhandClaude Opus 4.8 95c947be5b feat(deploy): LAN stack — run APESS in the room so the API can reach the boards
Fixes the reachability gap: a cloud-hosted API can't reach boards behind the
workshop NAT, so live board features (claim status, Build & flash, module
feeds) fail. This stack runs web + API on one box on the workshop WiFi, where
the API CAN reach each board's LAN IP.

- deploy/lan/: docker-compose (web + internal API), nginx.lan.conf (SPA +
  same-origin /api reverse-proxy with WS/SSE support), .env.example, README
  (launch, point the fleet's APESS_URL at the box, run the room).
- Web is IP-agnostic: nginx serves the SPA and proxies /api on the same origin,
  so no box IP is baked into the build.
- api.ts: API_BASE now supports a relative same-origin base ("/api"); wsUrl()
  derives ws scheme+host from the page for it (absolute prod base unchanged).
- Dockerfile.web: VITE_API_BASE build arg (empty=prod default, "/api"=LAN).

Verified E2E with a real `docker compose up`: SPA 200, /api/healthz ok,
self-register→unclaimed round-trip through the proxy, /api/ws → 101 Switching
Protocols, and 0 hosted-URL refs in the /api build. Web 220 + api 56 green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 17:25:06 -07:00
Omar SobhandClaude Opus 4.8 2c18e67c1b feat(onboarding): board re-attach — auto-heal reboots, resume lost browsers, instructor release
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]>
2026-07-08 17:16:14 -07:00
Omar SobhandClaude Opus 4.8 4d9d984b04 feat(uno-q): provision-fleet flashes the default boot animation
Folds the boot animation into the fleet pass — after onboarding each
board (env + self-register + timer/cron), provision-fleet now flashes the
default sketch (sketches/matrix_rain) via flash-sketch.sh. Best-effort: a
flash hiccup logs a warning but doesn't fail the board's onboarding.

- FLASH_DEFAULT=1 (default; 0 skips) and FLASH_SKETCH=<dir> knobs.
- Upfront guards for flash-sketch.sh + the sketch dir.
- provision() refactored to a single success tail (persistence → flash).

Verified on hardware: one pass reports both "ok — onboarding (cron)" and
"ok — boot animation (matrix_rain)".

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 15:46:27 -07:00
Omar SobhandClaude Opus 4.8 a2ccf519d4 feat(uno-q): flash-sketch helper + matrix_rain as the board boot default
- flash-sketch.sh: compile + flash a sketch dir onto a board's MCU over adb
  (push → arduino-cli compile with TMPDIR=/tmp → arduino-flash @ 0x80F0000).
  Defaults to sketches/matrix_rain; includes a fleet loop over adb devices.
  DRYs up the recipe and makes "set the board default" one command.
- Designate matrix_rain as the boot animation boards ship with (the flashed
  MCU sketch persists across power cycles). Documented in the sketches README
  + parent Files list.

Flashed matrix_rain to the board via the helper (821 bytes, verified E2E).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 15:40:40 -07:00
Omar SobhandClaude Opus 4.8 5f2456e5cb feat(uno-q): add bloom, breathe, checker effects to the matrix sampler
Rounds out the sampler with the remaining effects:
- bloom   — a filled diamond grows from the centre to full, then collapses.
- breathe — a dithered shimmer that swells and fades (density ramp via a
            fixed per-pixel threshold, since the matrix can't truly fade).
- checker — a checkerboard that inverts on each beat.

Full cycle is now rain -> knight-rider -> comet -> ripple -> bloom ->
breathe -> sparkle -> checker -> wipe. Compiled + flashed E2E (2057 bytes).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 14:51:40 -07:00
Omar SobhandClaude Opus 4.8 45a8feb8b4 feat(uno-q): add comet effect to the matrix sampler
A pixel that bounces off the walls trailing its last 5 cells (a diagonal
streak that bends on each bounce). Wired into the cycle between the
knight-rider sweep and ripple. Compiled + flashed E2E (1637 bytes).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 14:49:52 -07:00
Omar SobhandClaude Opus 4.8 2683c64c34 feat(uno-q): LED-matrix animation sketches (blue 13x8)
There was only a scroll-text template; add frame-based animations for the
Uno Q's built-in 13x8 monochrome-blue matrix. Both compiled (arduino:zephyr
0.51.0) and flashed E2E on hardware (OpenOCD @ 0x80F0000, per boards.txt).

- matrix_rain/     — digital rain: staggered per-column drops + trails.
- matrix_effects/  — sampler cycling rain / knight-rider / ripple / sparkle
                     / wipe.
- README: the grid->uint32[4] packing pattern (row-major, MSB-first), how to
  add an effect, and the compile/flash recipe (TMPDIR=/tmp trap + the correct
  0x80F0000 address vs QClaw's stale 0x8100000).

Note: the matrix is physically blue + on/off only — no colour/brightness in
software.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 14:47:53 -07:00
Omar SobhandClaude Opus 4.8 19593a637c feat(uno-q): fleet provisioner + printable QR sticker sheet
Two host-side helpers that close the operational gap between minting kit
codes and boards-in-hand.

- provision-fleet.sh: provision a whole fleet from fleet.csv (kit,serial).
  Per board it pushes the per-kit apess-node.env + apess-selfregister.sh
  over adb and enables the boot/refresh systemd timer (MODE=systemd, root)
  or an equivalent cron (@reboot + every 5 min; the no-root default).
  fleet.csv.example is the assignment template.
- gen-qr-sheet.sh: render the sticker CSV into a self-contained, printable
  QR sheet (A4, ~9/page). QRs are baked in as inline SVG via qrencode, so
  the HTML has zero external refs — prints offline. Each sticker: kit id,
  QR of the claim URL (/workshop?kit=..&code=..), and the 6-digit code.
- .gitignore: never commit kit-codes/ (env files hold FLEET_SECRET +
  claim codes) or fleet.csv.
- README: documents both, plus the one-command fleet path.

Verified: provision-fleet loop logic (header/comment/missing skips) and a
4-kit end-to-end sheet build (4 inline SVGs, 0 external refs).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 14:33:39 -07:00
Omar SobhandClaude Opus 4.8 a26185d47b feat(web): wire Module 1/2 live feed to the real node (not Web Serial)
In live mode the sense loop runs on the board (ZeroClaw on the Uno Q),
so the browser observes it rather than reading frames over Web Serial.

- useNodeFeed(teamId, enabled): subscribes to the node's SSE activity
  feed (openTeamActivity) and seeds liveness with a one-shot getNodeStatus
  (the per-team stream only emits status on change). Inert in sim mode.
- LiveBoardFeed: shows the board's real perception->reason->act activity
  + a live tally (agent runs / flashes / errors) + online indicator.
- Module 1/2 are now mode-aware: sim keeps the synthetic IMU feed +
  browser classification (the teaching sandbox); live shows LiveBoardFeed
  from the actual board. Proceed gates on board online (M1) / real board
  activity (M2) in live, unchanged in sim.

Note: this consumes the board's real activity stream (the signal the
Uno Q emits today). Streaming raw IMU frames for browser-side
classification in live mode would need a board-side sensor emitter
(ZeroClaw firmware) + an API frame relay — a separate piece.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 17:37:38 -07:00
Omar SobhandClaude Opus 4.8 71a255cf15 feat(admin): unclaimed-boards view — see powered-on boards awaiting claim
Surfaces the self-register pool in the instructor console so you can tell
at a glance which boards are up but not yet claimed.

- api: GET /nodes/unclaimed (admin-only; kit ids, never secrets) +
  broadcast unclaimed:update on self-register (added) and claim (removed).
  The WS connect snapshot now carries the current unclaimed kit ids
  (attachWs takes the pool), so a freshly opened console is populated
  without waiting for a change.
- web: useCollective tracks `unclaimed` (snapshot + unclaimed:update, with
  a REST getUnclaimed seed for the poll fallback); Admin shows an
  "Unclaimed" stat + a live "Unclaimed boards" panel of kit chips.

Verified live: empty → self-register adds the kit → claim removes it;
judge code is 401 on the endpoint.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 16:49:27 -07:00
Omar SobhandClaude Opus 4.8 8da6d1d7c1 test(deploy): add onboarding smoke-test script
verify-onboarding.sh runs the self-register + claim chain against a live
APESS API and asserts each HTTP status (correct/wrong fleet secret,
wrong code, unknown kit, successful claim, status, cleanup). Proven
end-to-end against a local API (7/7). bash 3.2-safe (no apostrophes in
${:?} messages, temp-file body capture).

  API=… FLEET_SECRET=… [ADMIN_CODE=…] deploy/verify-onboarding.sh

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 14:51:58 -07:00
Omar SobhandClaude Opus 4.8 5f70c8257e chore(deploy): pass FLEET_SECRET to the apess-api container
The API gates board self-registration on FLEET_SECRET; wire it through
docker-compose (host .env) alongside ADMIN_CODE/JUDGE_CODE and document
it in the deploy README.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 09:37:13 -07:00
Omar SobhandClaude Opus 4.8 3f6c6ab399 feat(uno-q): board self-register on boot + QR claim prefill (onboarding slice 3)
Board-side half of "preloaded + self-register + claim", plus the QR flow.

- apess-selfregister.sh (on-board): pairs locally for a token, discovers
  the LAN IP, and announces {kitId, url, token, claimCode} to APESS
  /nodes/self-register (x-fleet-secret gated). Retries until APESS is up;
  idempotent, safe on boot and on a timer.
- systemd/apess-selfregister.{service,timer}: self-register After the
  daemon, re-announce every 5 min so a DHCP lease change can't strand a
  board.
- apess-node.env.example: per-board identity (KIT_ID, CLAIM_CODE,
  FLEET_SECRET, APESS_URL).
- gen-kit-codes.sh (host): mint per-kit 6-digit codes, write the
  per-board env files, and emit the QR sticker CSV
  (…/workshop?kit=KIT-NN&code=NNNNNN).
- web: BoardClaim accepts initialCode; TeamRegistration pre-fills it from
  ?code= so scanning the kit QR fills kit + code — one tap to claim.
- deploy/uno-q/README: documents the whole self-serve onboarding path.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 09:36:37 -07:00
Omar SobhandClaude Opus 4.8 0442f7e865 feat(web): board claim wizard replaces Web-Serial connect (onboarding slice 2)
Attendee-facing half of the "preloaded + self-register + claim" flow.

- BoardClaim wizard: three physical bring-up steps (power → boot →
  enter code) + a claim-code field that calls POST /claim. On success
  the board shows as claimed; ClaimError surfaces the server's message
  (wrong code / not powered on / rate-limited).
- TeamRegistration: swaps the Web-Serial "connect device · 115200 baud"
  step for the wizard, with a "use the simulator instead" escape hatch
  (sim needs no board). serial.ts stays — Module1/2 still use it for the
  simulated sense path.
- EnvSetup: live self-test now polls GET /nodes/:teamId/status (board
  liveness) instead of reading Web-Serial frames; sim path unchanged.
- api client: claimBoard() + ClaimError + getNodeStatus().
- api: GET /nodes/:teamId/status (public per-team liveness).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 09:32:47 -07:00
Omar SobhandClaude Opus 4.8 14e08b3623 feat(api): board self-register + attendee claim (onboarding slice 1)
Adds the server side of the "preloaded + self-register + claim" board
onboarding so attendees bring a board online with no operator and no
admin code.

- claim.ts: an unclaimed pool keyed by kitId, holding {url, token,
  claimCode}; single-use claim with a constant-time code compare and a
  per-kit sliding-window rate limit (MAX_FAILS/WINDOW_MS) to blunt code
  guessing.
- POST /nodes/self-register — a booting board announces itself; gated
  by a shared FLEET_SECRET (baked into the image), not the admin code.
- POST /claim {kit, teamId, code} — public + rate-limited; validates
  the claim code, moves the bearer token straight from the pool into
  the node bridge (never touches the browser), binds the board to the
  team without clobbering an existing name/members, and emits the
  node:status + team:update the live feeds already consume.
- index.ts: wire the pool + FLEET_SECRET env.

22 new tests (claim unit + endpoint integration).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 15:28:14 -07:00
Omar SobhandClaude Opus 4.8 88789b3f5c feat(web): add staff sign-in entrance (judge / instructor) to landing
The landing page only routed attendees; /judge and /admin were
unlinked. Add a subtle "Staff sign-in ▾" disclosure in the header
linking to the judge and instructor consoles (both already gated by
AccessGate). Attendee hero is unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 15:24:18 -07:00
Omar SobhandClaude Opus 4.8 6688ced6b3 fix(web): name the Arduino Uno Q (4 GB) as the workshop node
Front-end still told the old UNO R4 WiFi story (Cortex-M4 / Renesas
RA4M1 / ~6 MB binary / UART→ESP32-S3 / browser-direct Web Serial).
The actual workshop node is the dual-brain Uno Q: ZeroClaw runs on
the quad-A53 Linux side, hosts a local Qwen model, and flashes the
on-board STM32U585 MCU it drives.

- Landing: hero, hardware spec tile, kit pickup, Stack cards, prereqs
- Lecture: Movement 3 (ZeroClaw) rewritten to the Uno Q architecture
- serial.ts + callers: device identity strings uno-r4-wifi -> uno-q
  (Web Serial transport mechanism unchanged)
- README stack line
- tests updated to match

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 13:02:27 -07:00
osobh 1ad6da64f7 Merge pull request 'fix(uno-q): supervisor survives reboot + recover actually launches it' (#6) from fix/supervisor-boot-robustness into main 2026-07-04 23:36:25 +00:00
Omar SobhandClaude Opus 4.8 21fc93556e fix(uno-q): make supervisor survive a reboot + recover actually launch it
A board power-cycle exposed two bugs that left the node dead after boot:

- recover-uno-q.sh step 3 ran `pgrep -f zeroclaw-supervisor`, which matches
  the pgrep's OWN shell (its args contain the supervisor path) — so it always
  concluded "already running" and never launched the supervisor. Bracket the
  pattern (`[z]eroclaw-supervisor.sh`) so only the real process matches.

- The supervisor's single-instance lock only checked that the locked pid was
  alive. After a reboot the stale pid can be recycled by an unrelated process,
  falsely blocking startup. Now require the live pid's /proc/<pid>/cmdline to
  actually be a supervisor before deferring — otherwise treat the lock as stale.

- Also export a full PATH in the supervisor for cron's minimal @reboot env.

Validated on hardware: discriminator matches a real supervisor and rejects
init's pid; fixed recover detects the running supervisor without spawning a
duplicate; board comes back healthy (gateway + llama up).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-04 16:36:01 -07:00
osobh 6cfce07f77 Merge pull request 'fix(api): self-healing node SSE subscription + live online status' (#5) from fix/node-bridge-auto-reconnect into main 2026-07-03 20:53:49 +00:00
Omar SobhandClaude Opus 4.8 c5c190c01f fix(api): self-healing node SSE subscription + live online status
The node bridge opened a board's /api/events stream once; when the board
dropped or its daemon restarted, the async reader just ended and never
reconnected. Registration still read `online` (set at register time), so a
board looked fine while its event stream was dead — participant prompts
appeared to vanish. Only a manual DELETE+POST re-register revived it.
Reproduced live: kill the on-board daemon → the feed went permanently blind.

- subscribeNodeEvents now reconnects with exponential backoff (1s→15s,
  reset on a healthy connection) until aborted, and reports each connect/drop
  via an onStatus callback.
- The bridge maps onStatus → a deduped node:status, so `online` tracks reality
  and the collective + participant feeds get offline/online transitions. A node
  that was offline at register is promoted once its stream connects.

Proven: unit tests for drop→reconnect, dedup, and offline-at-register
promotion; and live against the board — killing the daemon now yields
node:status false→true on the participant feed with no re-register.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-03 13:53:29 -07:00
osobh 64a646a5d5 Merge pull request 'feat(uno-q): disconnect resilience — on-board supervisor + host recovery' (#4) from feat/uno-q-disconnect-recovery into main 2026-07-03 20:27:41 +00:00
Omar SobhandClaude Opus 4.8 8130b0e30a feat(uno-q): disconnect resilience — on-board supervisor + host recovery
A sudden USB/adb drop breaks the Uno Q node in ways that don't self-heal:
adb tunnels vanish, held-shell services die, llama wedges (alive but not
serving), and flashes silently stop landing. systemd is the clean fix with
root — but the dev board's account is expired (no sudo) and has no user
session bus, so neither system nor user units run. These are the no-root
equivalents, both validated on hardware:

- zeroclaw-supervisor.sh — on-board watchdog. Polls the /health ENDPOINTS
  (a wedged process passes pgrep but fails here) and restarts llama / the
  daemon on death or wedge. Launches children with `setsid … exec` so they
  survive the launching shell — the property `nohup … &` in adb shell lacks.
  Startup grace avoids reaping llama mid-cold-load; single-instance lock;
  no per-restart shell leak. Proven: kill -9 the daemon → auto-restarted.
  Boot-persisted via `@reboot` crontab (no root).

- recover-uno-q.sh — host-side. After the board is back, re-does adb + both
  tunnels (out :8080, back :8090 shim), ensures the supervisor is running,
  and health-checks every hop by endpoint. Proven end-to-end after a
  simulated tunnel drop.

README: new Resilience section documenting the no-root reality + both tools.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-03 13:24:36 -07:00
osobh 56418f1742 Merge pull request 'feat(apess): real per-team activity counts + judge board evidence' (#3) from feat/stage-e-real-activity into main 2026-07-03 16:20:12 +00:00
Omar SobhandClaude Opus 4.8 56324d7c89 feat(apess): real per-team activity counts + judge board evidence
Stage E deeper — derive real signal from the board event stream:

- Collective reducer tallies per-team {calls, flashes, errors} from
  node:activity (thinking→calls, flash→flashes, error→errors); exposed
  as `counts` alongside the existing activity feed.
- Admin gains a room-wide "Flashes" pulse stat.
- Judge review card shows a live "Board evidence" panel — streams the
  reviewed team's own board activity via its SSE feed, so scoring can
  reference real on-device work, not just the submitted code.

Tests: front-end 182, typecheck clean, prod build passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-03 09:19:53 -07:00
osobh 66785601a2 Merge pull request 'feat(deploy): Uno Q workshop-node provisioning kit' (#2) from feat/uno-q-provisioning into main 2026-07-03 16:09:28 +00:00
Omar SobhandClaude Opus 4.8 89602064c6 feat(deploy): Uno Q workshop-node provisioning kit
Codify the verified board setup into a repeatable kit under deploy/uno-q/:

- config.template.toml — the tested node config: cloud / cloud+fallback /
  on-board-Qwen providers, three agents (default / cloud / local) on the
  hardware risk profile, gateway, peripherals. Secrets stripped; cloud
  endpoint substituted at provision time.
- provision-uno-q.sh — one-shot adb-driven provisioner (no root): install
  config, start llama-server + `zeroclaw daemon`, pair for a bearer token,
  and POST /nodes to APESS.
- systemd/{zeroclaw-llama,zeroclaw-daemon}.service — production units
  (daemon, not `gateway start`, so peripheral tools register).
- README.md — dev (adb) and production (systemd + LAN bind) runbooks.

The individual steps are hardware-verified: all three agent aliases resolve
and route correctly on the board (local→Qwen, cloud→cloud, default→cloud
with Qwen fallback). Full script orchestration + live phone-home land once
the api (with /nodes) is redeployed.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-03 06:50:06 -07:00
osobh a82b8bd46c Merge pull request 'feat(apess): wire real ZeroClaw nodes + live/sim + cloud-local fallback' (#1) from feat/zeroclaw-node-integration into main 2026-07-03 13:39:31 +00:00
Omar SobhandClaude Opus 4.8 100b8fd946 feat(apess): route a team's provider/fallback choice to a board agent alias
Close the tune→deploy loop without live config mutation. The gateway
already supports per-request `?agent=` selection, so map the harness to a
pre-provisioned agent alias and route the prompt to it:
  - local primary            → `local`  (on-board Qwen only)
  - cloud primary + fallback  → `default` (cloud + Qwen fallback)
  - cloud primary, no fallback→ `cloud`  (cloud only)

- lib/harness: harnessToAgent(h).
- api: sendPrompt(node, msg, agent?) → `?agent=`; bridge.prompt +
  POST /nodes/:teamId/prompt accept an optional `agent`.
- lib/api.sendPrompt(teamId, msg, agent?); BuildFlash derives the alias
  from the live harness.

Boards must be provisioned with the `default`/`cloud`/`local` aliases
(our board currently has `default`); that lands with the provisioning kit.

Tests: front-end 180, api 29, typecheck clean, prod build passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-03 06:01:33 -07:00
Omar SobhandClaude Opus 4.8 28a0965108 feat(apess): surface real board activity on the instructor dashboard
Extend the collective feed to consume node:status / node:activity: the
reducer now tracks per-team board online state and a bounded,
most-recent-first activity feed (preserved across snapshots). Admin gains
a "Boards live" stat and a "Board activity" panel (new BoardActivity
component) showing real generate/compile/flash events across teams.

Tests: front-end 177, typecheck clean, prod build passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-03 05:56:39 -07:00
Omar SobhandClaude Opus 4.8 f21bbdf31e feat(apess): wire real ZeroClaw nodes + live/sim mode + cloud→local fallback
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]>
2026-07-03 05:52:51 -07:00
Omar SobhandClaude Opus 4.8 d58065d292 fix(deploy): use single-level apess-api.redclaw.dev for the API host
api.apess.redclaw.dev is a multi-level subdomain that Cloudflare Universal
SSL and the *.redclaw.dev origin cert don't cover (edge TLS handshake fails).
Switch the API host to apess-api.redclaw.dev, which the existing edge + origin
certs cover with no paid Cloudflare features. Updates the client API base
default, the Traefik router host, and deploy docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-17 08:23:17 -07:00
Omar SobhandClaude Opus 4.8 0eea6efdec Merge: all remaining screens + the collective (backend + admin/judge)
Brings APESS 2026 from 2/9 to 9/9 screens live, adds the api/ collective
backend (Express + better-sqlite3 + ws) and its deploy artifacts.

Frontend: 140 tests; Backend: 13 tests; both typecheck + lint clean.
Docker image built and smoke-tested end-to-end (REST auth + WebSocket).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-16 21:21:35 -07:00
Omar SobhandClaude Opus 4.8 b3567718fc build(deploy): apess-api image + compose + Traefik router (B5)
- api/Dockerfile: node:22-alpine, builds better-sqlite3 from source for musl
  (prebuilds are glibc), runs tsx; healthcheck on /healthz
- .dockerignore: stops host node_modules (wrong-platform native binaries)
  from shadowing the in-container build — the root cause of an ERR_DLOPEN
  'Exec format error' found while validating the image
- docker-compose.yml: apess-api service on clawbooks-net + apess-data volume,
  ADMIN_CODE/JUDGE_CODE/CORS_ORIGIN env
- traefik/apess-api.yml: file-provider router for api.apess.redclaw.dev
  (reuses the *.redclaw.dev cert; WS proxied automatically)
- deploy/README: API prerequisites (.env codes, DNS, router), deploy/verify
  (REST + WS smoke tests), rollback notes; .env gitignored

Validated by building + running the image: /healthz=ok, PUT/GET /teams with
auth (401 without code), WS snapshot on connect, WS 4401 on bad code.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-16 19:39:22 -07:00
Omar SobhandClaude Opus 4.8 bd3250c9c7 feat: /judge review + scoring + leaderboard — TDD
Final screen — replaces the last WorkshopStub; App.tsx now has zero stubs:
- SubmissionList: live review queue with scored markers + selection
- AddReview: read-only render of a submitted 5-layer ADD
- ScoreForm: 0-10 rubric per layer, auto-summed total, notes
- Leaderboard: ranked teams by average judge total
- Judge page (behind name+code AccessGate): queue + review/score + leaderboard,
  refreshes on scoring and on any collective event

21 new tests; suite 140/140 green, typecheck + lint clean, build OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-16 19:31:36 -07:00
Omar SobhandClaude Opus 4.8 6c07788fab feat: /admin instructor dashboard — TDD
- AccessGate: shared code (+optional name) gate for /admin and /judge,
  sessionStorage-backed
- TeamCard: pure 15-grid tile — phase dots, stats line, submitted/judged
  badges, stale dimming
- useCollective: REST-seeded + WS-live reducer of all teams/submissions,
  degrades to polling when the socket is silent
- useNow: ticking clock hook (effect-driven) for stale dimming
- Admin page: aggregate strip + 15-card grid behind the access gate

19 new tests; suite 126/126 green, typecheck + lint clean, build OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-16 19:27:08 -07:00
Omar SobhandClaude Opus 4.8 b18551f120 feat: collective client sync — api.ts + useCollectiveSync (B2)
- src/lib/api.ts: typed client for the collective — pushTeam/pushSubmission
  (public), getTeams/getSubmissions/getSubmission/postScore/getLeaderboard
  (code-gated), and openCollective WS with auto-reconnect
- src/lib/useCollectiveSync.ts: offline-first sync mounted once in App —
  projects the store to a TeamSnapshot, pushes on phase change + submission,
  throttles stat churn, and routes failures to a sessionStorage outbox that
  retries on interval + window 'online'. Never blocks the participant flow.

11 new tests (incl. backend-down path); suite 112/112 green, typecheck + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-16 19:21:41 -07:00
Omar SobhandClaude Opus 4.8 9808e6958e feat(api): collective backend — Express + better-sqlite3 + ws — TDD
New api/ service powering admin + judge for the single-day event:
- db.ts: SQLite store (teams/submissions/scores), upserts, leaderboard;
  POST /submissions flips the team's add phase
- app.ts: REST — public PUT /teams/:id + POST /submissions (broadcast),
  code-protected GET /teams, /submissions[/:team], POST /scores, /leaderboard
- auth.ts: shared admin/judge access codes (header/bearer/?code=), const-time compare
- hub.ts + ws.ts: WS feed at /ws — auth via ?code=, snapshot on connect,
  team:update / submission:new / score:new broadcasts, heartbeat
- index.ts: http + ws wiring; types.ts mirrors the client DTOs

13 tests (REST via supertest + WS integration) green; typecheck clean;
boot smoke test: /healthz=ok, protected routes 401 without a code.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-16 19:19:24 -07:00
Omar SobhandClaude Opus 4.8 ca023cdaa5 feat: stable teamId in store + shared collective DTOs (B-prep)
- 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]>
2026-06-16 19:14:09 -07:00
Omar SobhandClaude Opus 4.8 7c141dcd89 feat: participant flow screens setup/module1/module2/add — TDD
All four workshop phases are now real screens, replacing WorkshopStub:
- EnvSetup (/workshop/setup): device check + serial self-test (no stats
  pollution) + provider/model selection; gated Proceed
- Module1 (/workshop/module1): live IMU feed via useSerial, actor map,
  ADD Layer 1 capture; gated on observed frames + L1 goal
- Module2 (/workshop/module2): harness tuner + live TOML preview, test
  triggers that classify against the tuned threshold, ADD L2/L3
- AddBuilder (/workshop/add): ADD L4/L5, assembled AddDocument, print-to-PDF
  export, deterministic submission code; completes the run
- New components: ActorMap, HarnessTuner, TriggerButtons, AddDocument

23 new tests; suite 86/86 green, typecheck + lint clean, build OK.
Only /admin and /judge remain stubbed (need the backend).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-16 19:13:07 -07:00
Omar SobhandClaude Opus 4.8 965af7e5c0 feat: shared workshop components (ADD form, stats, feed, harness) — TDD
- AddLayerForm: keystone ADD capture — string layers (L2-L5) as textarea,
  object layers (L1) as per-field inputs; writes straight to the store
- StatsTally: live classified-frame counts
- LiveFeed: presentational IMU readout (props-driven)
- HarnessTomlPreview: live harness.toml render
- HarnessProviderSelect: provider toggle group + model field
- ui/textarea primitive

14 new tests; suite 63/63 green, typecheck + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-16 19:07:20 -07:00
Omar SobhandClaude Opus 4.8 66191217ec feat: Web Serial bridge + harness/submission libs — TDD
- src/lib/serial.ts: serialSupported, parseLine, classifyFrame (pure),
  requestPort with deterministic mock fallback for tests/unsupported browsers
- src/lib/useSerial.ts: React hook streaming frames into stats via recordEvent,
  classifying against the live (re-readable) harness; inject() for test triggers
- src/lib/harness.ts: harnessToToml renderer
- src/lib/submission.ts: deterministic makeSubmissionCode
- TeamRegistration.onConnect now uses requestPort (sync mock fallback keeps
  the existing flow test green)

20 new tests; suite 49/49 green, typecheck + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-16 19:05:08 -07:00
Omar SobhandClaude Opus 4.8 8163dd7829 feat: Lecture page (/lecture, 5 movements) — TDD
Replaces the WorkshopStub for /lecture with a real content screen:
5 movement sections (agency, perception loop, ZeroClaw, failure modes,
edge vs cloud), mirroring the Landing visual idiom. No PhaseStrip — the
lecture is not a workshop phase. 6 new tests; suite 29/29 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-16 19:01:53 -07:00
Omar Sobh e64bce81ab fix(perf): non-blocking Google Fonts load + chmod assets in image
Two issues surfaced when the deployed site was loaded:

1. favicon.svg shipped from public/ with macOS mode 600 (~/owner-only).
   nginx runs as the 'nginx' user inside the alpine image and returned
   403 for the icon. Add a chmod sweep at the end of the build stage so
   every shipped asset is 644 + dirs 755 regardless of host umask.

2. <link rel="stylesheet"> to fonts.googleapis.com is render-blocking.
   When the user's network is slow to reach Google Fonts (or blocks it),
   the page spins indefinitely on first paint. Switch to the canonical
   rel=preload + onload swap pattern, with a <noscript> fallback for
   JS-off browsers. Tailwind's font-family stack already cascades to
   ui-serif / ui-monospace, so type still renders before the swap.
2026-06-10 04:41:45 -05:00
Omar Sobh 31f1cd866f fix(deploy): use file-provider router instead of docker labels
The docker-label router (apess-web@docker) was being silently dropped
by Traefik after subsequent reloads, even though all labels and the
container's network attach were correct. Falling back to the
ClawBooks landing's catch-all PathPrefix(`/`) meant apess.redclaw.dev
served the wrong content after the first ~5 minutes.

Switch to an explicit file-provider definition in apess.yml with
priority: 1000 (well above landing's priority: 1) so the router is
authoritative regardless of docker-discovery races. The compose file
now just runs the container; routing is owned by the dynamic config.
2026-06-09 23:02:02 -05:00
Omar Sobh 6af793b3cf feat: Landing (Screen 0) + Team Registration (Screen 1) — TDD
Closes the first two PRD screens with shadcn primitives (button, input,
card, badge) and the standing TDD-first discipline.

Landing (/) — PRD §5.1 Screen 0
- Sticky nav with workshop CTA and lecture link
- Hero with eyebrow badge, headline, sub, dual CTA + 4-stat strip
- Programme table covering all 7 day-of moves (13:00 → 19:00)
- Speaker card for Omar Sobh + RedClaw Systems
- 3-card tech stack (Rust on Cortex-M4 · Anthropic+Groq · Web Serial)
- Prerequisites + closing CTA
- 5 tests, all passing

Team Registration (/workshop) — PRD §5.1 Screen 1
- PhaseStrip across top, Phase 1 badge eyebrow
- Team name + member chips (Enter to add, x to remove, capped at 5)
- KitSelector 5x3 grid of KIT-01..KIT-15 with aria-pressed
- Mock device-connect button (real Web Serial deferred to next slice)
- ?kit= URL param pre-selects kit (QR sticker entrypoint)
- Proceed gated until name + 1+ member + device connected
- On Proceed: marks phases.reg = true, navigates to /workshop/setup
- 6 tests, all passing

MemberChips (5 tests) + KitSelector (3 tests) extracted as reusable
components with own test coverage.

Scoped react-refresh/only-export-components off for src/components/ui/**
(shadcn vendored primitives export their cva variants alongside the
component — standard project pattern).

23/23 tests green · build 286KB JS, 90KB gzip · lint clean.
2026-06-09 22:55:54 -05:00
Omar Sobh 4059ea9227 build: pin pnpm 10.33.0 + resilient install in Dockerfile
Initial gw-03 build failed with pnpm 10 lockfile-policy rejection inside
the container (different policy config from host). Pinning packageManager
and falling back to --no-frozen-lockfile when the policy gate rejects
unblocks the multi-stage build without weakening lockfile integrity in
day-to-day pnpm install on the host.
2026-06-09 18:55:54 -05:00
Omar Sobh 6ef3de7a69 deploy: align Traefik TLS paths with gw-03 ssl/ convention 2026-06-09 18:53:29 -05:00
Omar Sobh 394e226975 Add gw-03 deploy artifacts (Docker + Traefik + nginx)
Stages the production deploy bundle next to the existing ClawBooks stack
on zeroclaw-gw-03. Reuses ClawBooks' Traefik v3.6 via shared
clawbooks-net Docker network, no separate proxy.

- deploy/Dockerfile.web: multi-stage Node 22 build -> nginx 1.27 alpine
- deploy/nginx.conf: SPA history fallback, asset caching, /healthz probe
- deploy/docker-compose.yml: apess-web service with Traefik labels for
  apess.redclaw.dev on the websecure entrypoint
- deploy/traefik/apess.yml: TLS dynamic config pointing at the
  *.redclaw.dev Cloudflare Origin Certificate (to be installed at
  /etc/ssl/redclaw/apess.{pem,key} on gw-03)
- deploy/README.md: prereqs (CF DNS + Origin Cert), deploy commands,
  verification, rollback

No infra changes applied yet -- this is the artifact bundle. Deploy
pending three out-of-repo prereqs: CF DNS records, CF Origin Cert
issuance, and rsync+up on gw-03.
2026-06-09 18:32:32 -05:00