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
+37 -55
View File
@@ -1,79 +1,61 @@
import { Link, useNavigate } 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 { OpenYourNode } from '@/components/OpenYourNode'
import { DomainPicker } from '@/components/DomainPicker'
import { PanelHeading, PanelCard, ProceedButton } from '@/components/cockpit/PanelChrome'
import { useSession } from '@/store/session'
export function EnvSetup() {
const navigate = useNavigate()
const device = useSession((s) => s.device)
const domain = useSession((s) => s.domain)
const add = useSession((s) => s.add)
const setAddLayer = useSession((s) => s.setAddLayer)
const completePhase = useSession((s) => s.completePhase)
const ready = device.connected && domain.trim().length > 0
const onProceed = () => {
// Carry the domain into Layer 1 as a starting draft (only if untouched).
if (!add.L1.trim() && domain.trim()) {
setAddLayer('L1', `Domain: ${domain.trim()}. Events: an impact spike, a sustained sway, a stale sensor.`)
}
completePhase('setup')
navigate('/workshop/module1')
}
return (
<main className="min-h-screen bg-background">
<header className="px-8 py-5 border-b border-border flex items-center justify-between">
<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>
<section>
<PanelHeading
eyebrow="PHASE 2 OF 5 · ~15 MIN"
title="Meet your agent"
intro="Your board runs the APESS agent — a Claude-powered agent on the edge. It reasons about your domain, drives the board's own devices, and keeps working when the cloud drops. Open it to explore, then name the domain it's for."
/>
{/* Open your agent */}
<PanelCard className="mt-9">
<div className="text-[17px] font-semibold">Open your agent to explore</div>
<div className="mt-4">
<OpenYourNode variant="hero" />
</div>
<Link to="/workshop" className="font-mono text-[11px] text-muted-foreground hover:text-foreground tracking-widest uppercase">
Team registration
</Link>
</header>
</PanelCard>
<PhaseStrip active="setup" />
<section className="px-8 py-10 max-w-3xl mx-auto space-y-6">
<div>
<Badge variant="secondary" className="font-mono text-[10px] tracking-widest uppercase mb-2">
Phase 2 of 5 · ~15 min
</Badge>
<h1 className="text-3xl font-bold tracking-tight">Meet your agent</h1>
<p className="text-sm text-muted-foreground mt-2 max-w-2xl leading-relaxed">
Your board now runs the <span className="font-medium text-foreground">APESS agent</span> a
Claude-powered agent living on the edge. It reasons about your domain, drives the board&rsquo;s
own devices, and keeps working when the cloud drops by falling back to an on-board model.
Open it to explore, then name the domain it&rsquo;s for.
</p>
{/* Pick your domain — emphasized: this seeds all five layers */}
<PanelCard emphasized className="mt-[22px]">
<div className="text-[17px] font-semibold">Pick your domain</div>
<p className="mt-1.5 text-[14px] leading-[1.5] text-ink-3">
This one line seeds all five layers of your Agent Design Document the domain your agent
serves and the events it must notice.
</p>
<div className="mt-4">
<DomainPicker />
</div>
</PanelCard>
{/* 1 — Open your agent (hero) */}
<Card>
<CardHeader>
<CardTitle className="text-base">Open your agent to explore</CardTitle>
</CardHeader>
<CardContent>
<OpenYourNode variant="hero" />
</CardContent>
</Card>
{/* 2 — Pick your domain */}
<Card>
<CardHeader>
<CardTitle className="text-base">Pick your domain</CardTitle>
</CardHeader>
<CardContent>
<DomainPicker />
</CardContent>
</Card>
<div className="flex justify-end pt-4">
<Button size="lg" disabled={!ready} onClick={onProceed}>
Proceed to Module 1
</Button>
</div>
</section>
</main>
<div className="mt-9 flex justify-end">
<ProceedButton disabled={!ready} onClick={onProceed}>
Proceed to Module 1
</ProceedButton>
</div>
</section>
)
}