feat(web): two-pane "cockpit" redesign with a live board rail

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]>
This commit is contained in:
Omar Sobh
2026-07-23 07:38:14 -07:00
co-authored by Claude Opus 4.8
parent 6c82b79b01
commit 1a39457940
28 changed files with 1240 additions and 434 deletions
+105 -103
View File
@@ -1,124 +1,126 @@
import { Link } from 'react-router-dom'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { PhaseStrip } from '@/components/PhaseStrip'
import { useNavigate } from 'react-router-dom'
import { AddLayerForm } from '@/components/AddLayerForm'
import { AddDocument } from '@/components/AddDocument'
import { OpenYourNode } from '@/components/OpenYourNode'
import { PanelHeading, ProceedButton } from '@/components/cockpit/PanelChrome'
import { ADD_LAYERS } from '@/lib/addLayers'
import { useSession } from '@/store/session'
import { makeSubmissionCode } from '@/lib/submission'
export function AddBuilder() {
const navigate = useNavigate()
const team = useSession((s) => s.team)
const add = useSession((s) => s.add)
const submission = useSession((s) => s.submission)
const setSubmission = useSession((s) => s.setSubmission)
const completePhase = useSession((s) => s.completePhase)
const complete =
add.L1.trim().length > 0 &&
add.L2.trim().length > 0 &&
add.L3.trim().length > 0 &&
add.L4.trim().length > 0 &&
add.L5.trim().length > 0
const onExport = () => window.print()
const onSubmit = () => {
const code = makeSubmissionCode(team, add)
setSubmission({ code, submittedAt: new Date().toISOString() })
completePhase('add')
// best-effort push to the collective is wired in the sync layer (Part B)
}
const complete = ADD_LAYERS.every(({ key }) => add[key].trim().length > 0)
const submitted = !!submission.code
return (
<main className="min-h-screen bg-background">
<header className="px-8 py-5 border-b border-border flex items-center justify-between print:hidden">
<div className="font-mono text-xs tracking-widest uppercase">
APESS <span className="text-primary font-bold">2026</span>
<span className="text-muted-foreground"> · Workshop</span>
</div>
<Link to="/workshop/module2" className="font-mono text-[11px] text-muted-foreground hover:text-foreground tracking-widest uppercase">
Module 2
</Link>
</header>
const onExport = () => window.print()
const onSubmit = () => {
setSubmission({ code: makeSubmissionCode(team, add), submittedAt: new Date().toISOString() })
completePhase('add')
}
<div className="print:hidden">
<PhaseStrip active="add" />
</div>
<section className="px-8 py-10 max-w-5xl mx-auto space-y-6">
<div className="print:hidden">
<Badge variant="secondary" className="font-mono text-[10px] tracking-widest uppercase mb-2">
Phase 5 of 5 · ~90 min · deadline 19:00
</Badge>
<h1 className="text-3xl font-bold tracking-tight">Module 3 · Harness, Loops &amp; submit</h1>
<p className="text-sm text-muted-foreground mt-2 max-w-xl">
Finish Layers 4 and 5 how your node reasons and how it runs over time review the assembled
Agent Design Document, export a PDF, and submit before the deadline.
// ── Submission finale (design panel6) ──
if (submitted) {
return (
<section className="print:hidden">
<div className="mt-5 rounded-[18px] border border-[var(--green-border-2)] bg-[linear-gradient(180deg,var(--green-bg),var(--surface))] px-12 py-14 text-center">
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-[var(--green)] text-[32px] text-white">
</div>
<h1 className="mt-6 text-[44px] font-semibold tracking-[-0.02em]">ADD submitted</h1>
<p className="mx-auto mt-3.5 max-w-[460px] text-[18px] leading-[1.5] text-ink-2">
Team <strong className="font-semibold text-ink">{team.name || 'Matrix'}</strong> ·{' '}
{(team.name || 'crimson-node').toLowerCase().replace(/\s+/g, '-')} your Agent Design Document
is in for judging. Your node keeps running the design you just shipped.
</p>
<div className="mt-3">
<OpenYourNode variant="inline" />
<div className="mt-7 inline-flex flex-wrap justify-center gap-2.5 font-mono text-[11px] tracking-[0.1em] text-[var(--green)]">
{ADD_LAYERS.map(({ n, title }) => (
<span key={n} className="rounded-md border border-[var(--green-border)] px-[11px] py-1.5">
L{n} {title.split(' ')[0].toUpperCase()}
</span>
))}
</div>
<div className="mt-8">
<button
type="button"
onClick={() => navigate('/workshop')}
className="rounded-[9px] border border-line-2 bg-surface px-[22px] py-2.5 text-[15px] text-ink transition-colors hover:border-blue hover:text-blue-ink"
>
Back to start
</button>
</div>
</div>
<div className="grid lg:grid-cols-2 gap-6 print:hidden">
<AddLayerForm
layer="L4"
title="ADD · Layer 4 — Harness (where each decision runs)"
description="Which decisions run on the board, which escalate to the cloud — and what still works with no network at all. Describe the degradation path, not just the happy path."
placeholder={
'Routine checks: on-board model, no network needed.\n' +
'Ambiguous or high-consequence calls: escalate to the cloud model.\n\n' +
'Degradation path:\n' +
'• cloud slow or rate-limited → fall back on-board, note reduced confidence\n' +
'• no network at all → keep sensing, logging and safing locally; queue anything that needs escalation\n' +
'• on-board model unavailable → stop actuating, alert, keep recording'
}
/>
<AddLayerForm
layer="L5"
title="ADD · Layer 5 — Loops (cadence, and what happens when a cycle fails)"
description="How often it checks, what it reports by exception — and how the loop behaves when a cycle fails: stale readings, missed ticks, partial data."
placeholder={
'Heartbeat every 30 s; sample the sensor each minute; report only on exception; daily summary.\n\n' +
'When a cycle fails:\n' +
'• missed tick → skip, do not back-fill invented data\n' +
'• partial data → report what is missing, not an average of what is left\n' +
'• N consecutive failures → escalate to a human and stop acting on the readings'
}
/>
</div>
<Card className="print:border-0 print:shadow-none">
<CardHeader className="print:hidden flex-row items-center justify-between space-y-0">
<CardTitle className="text-base">Assembled document</CardTitle>
<Button variant="outline" size="sm" onClick={onExport}>Export PDF</Button>
</CardHeader>
<CardContent className="print:p-0">
<AddDocument />
</CardContent>
</Card>
<div className="flex items-center justify-between gap-4 pt-2 print:hidden">
{submitted ? (
<div className="border border-teal/40 bg-teal/5 rounded-md px-4 py-3">
<div className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground">Submitted</div>
<div className="font-mono text-sm font-bold text-teal">{submission.code}</div>
</div>
) : (
<span className="text-xs text-muted-foreground">
{complete ? 'All five layers complete — ready to submit.' : 'Complete all five layers to submit.'}
</span>
)}
<Button size="lg" disabled={!complete || submitted} onClick={onSubmit}>
{submitted ? 'Submitted ✓' : 'Submit ADD'}
</Button>
</div>
</section>
</main>
)
}
return (
<section>
<div className="print:hidden">
<PanelHeading
eyebrow="PHASE 5 OF 5 · ~90 MIN · DEADLINE 19:00"
title="Module 3 · Harness, Loops & submit"
intro="Finish Layers 4 and 5 — how your node reasons and how it runs over time — review the assembled Agent Design Document, export a PDF, and submit before the deadline."
size={44}
/>
</div>
<div className="mt-8 grid gap-4 md:grid-cols-2 print:hidden">
<AddLayerForm
layer="L4"
title="ADD · Layer 4 — Harness"
description="Which decisions run on the board, which escalate — and the degradation path, not just the happy path."
placeholder={
'Routine checks: on-board model, no network needed.\n' +
'Ambiguous or high-consequence calls: escalate to the cloud model.\n\n' +
'Degradation path:\n' +
'• cloud slow → fall back on-board, note reduced confidence\n' +
'• no network → keep sensing, logging and safing locally\n' +
'• on-board model down → stop actuating, alert, keep recording'
}
/>
<AddLayerForm
layer="L5"
title="ADD · Layer 5 — Loops"
description="How often it checks, what it reports by exception — and how the loop behaves when a cycle fails."
placeholder={
'Heartbeat every 30 s; sample each minute; report only on exception.\n\n' +
'When a cycle fails:\n' +
'• missed tick → skip, do not back-fill invented data\n' +
'• partial data → report what is missing\n' +
'• N failures → escalate to a human and stop acting'
}
/>
</div>
<div className="mt-5 rounded-[14px] border border-line bg-surface p-[26px_28px]">
<div className="flex items-center justify-between print:hidden">
<div className="text-[19px] font-semibold">Assembled document</div>
<button
type="button"
onClick={onExport}
className="rounded-lg border border-line-2 bg-surface px-4 py-2 text-[14px] text-ink transition-colors hover:border-blue hover:text-blue-ink"
>
Export PDF
</button>
</div>
<div className="mt-[18px] print:mt-0">
<AddDocument />
</div>
</div>
<div className="mt-8 flex items-center justify-between gap-4 print:hidden">
<span className={complete ? 'text-[15px] text-[var(--green)]' : 'text-[15px] text-[var(--muted-2)]'}>
{complete ? 'All five layers complete — ready to submit.' : 'Complete all five layers to submit.'}
</span>
<ProceedButton disabled={!complete} onClick={onSubmit}>
Submit ADD
</ProceedButton>
</div>
</section>
)
}