Rebuild the operator console on the ethos shell

Nine screens against orchestra-ui-spec.md and the accepted mockups, on
the ethos design system: signal violet #8F7AE5, the routing fork motif,
the 64px rail and 56px top bar, mono for every machine value and sans
for every human one.

Each screen was built by its own agent against a fixed foundation, so
the shell, tokens and primitives have one author and the screens cannot
drift into nine dialects.

Real data only. Where no endpoint exists the screen says which one it
needs instead of inventing a value. That is most of what was learned
here: Steer / Correct is disabled because nothing records a human
decision from the web, take-control is disabled because nothing
forwards keystrokes to a pane, context occupancy is missing from three
screens, and projects can show no repo, remote, quality gate or
verification policy because those live only in config.jsonc.

Three bugs the render caught that no computed value would have:

The previous stylesheet fought every shared class name and leaked
properties the new rules never mention, which is how position:fixed
survived on .topbar. It is now scoped under .legacy and applies only to
the login route, which also stops its green accent and its
backdrop-filter from reaching the console.

Go marshals a zero time.Time as "0001-01-01T00:00:00Z" and omitempty
does not omit a struct, so absent timestamps arrived populated-looking
and rendered as "739855d ago". Stripped once at the API boundary every
screen reads through, with a test.

Long machine ids overflowed their cards and painted under the next one.

Verified by rendering: chromium screenshots of the dashboard, tasks,
task detail, terminal, workers and review at 1440px, and the dashboard
at 390px. Geist is still not on disk, so both stacks fall back to the
system faces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
This commit is contained in:
2026-08-29 02:44:43 +04:00
parent 118ac9fbcb
commit 34f3c2888f
39 changed files with 6958 additions and 573 deletions
+296
View File
@@ -0,0 +1,296 @@
import { useMemo, useState } from 'react'
import { Link } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { api } from '../api/client'
import type { Task, TaskState, Worker } from '../api/types'
import { Chip, Dot, Empty, EndpointGap, M, Panel } from '../components/Primitives'
import './Projects.css'
/** Orchestra has no projects endpoint. The registry that defines a project —
* repo, remote, quality gate, verification policy, machine affinity — lives in
* the coordinator's config.jsonc and is never served over HTTP. What this
* screen shows is derived from work that actually exists: the tasks in
* /v1/ui/overview carry a project, and the workers in the federation registry
* declare which projects they will accept. Everything else is a stated gap,
* not a plausible-looking guess. */
const ATTENTION: TaskState[] = ['needs_attention', 'blocked', 'failed']
interface ProjectRow {
id: string
tasks: Task[]
counts: Record<TaskState, number>
attention: number
active: number
/** Newest capture/check the coordinator has for this project's sessions.
* A task carries no updated_at, so this is the only honest recency signal
* the overview projection actually contains. */
lastSeen?: string
workers: Worker[]
}
function fmt(at?: string) {
if (!at) return undefined
const d = new Date(at)
return Number.isNaN(d.getTime()) ? at : `${d.toISOString().slice(0, 16).replace('T', ' ')} UTC`
}
function newest(a?: string, b?: string) {
if (!a) return b
if (!b) return a
return a > b ? a : b
}
function derive(tasks: Task[], workers: Worker[]): ProjectRow[] {
const rows = new Map<string, ProjectRow>()
for (const task of tasks) {
const id = task.project || '(unset)'
let row = rows.get(id)
if (!row) {
row = {
id,
tasks: [],
counts: {} as Record<TaskState, number>,
attention: 0,
active: 0,
workers: workers.filter((w) => (w.supported_projects || []).includes(id)),
}
rows.set(id, row)
}
row.tasks.push(task)
row.counts[task.state] = (row.counts[task.state] || 0) + 1
if (ATTENTION.includes(task.state)) row.attention++
if (task.state === 'leased') row.active++
row.lastSeen = newest(
row.lastSeen,
newest(task.last_session?.captured_at, task.last_session?.checked_at),
)
}
return [...rows.values()].sort(
(a, b) => b.attention - a.attention || b.active - a.active || a.id.localeCompare(b.id),
)
}
function stateChips(counts: Record<TaskState, number>) {
const order: TaskState[] = [
'leased',
'queued',
'in_review',
'needs_attention',
'blocked',
'failed',
'completed',
]
return order
.filter((state) => counts[state])
.map((state) => (
<Chip
key={state}
tone={
state === 'leased'
? 'accent'
: state === 'completed'
? 'done'
: state === 'needs_attention'
? 'warn'
: ATTENTION.includes(state)
? 'fault'
: undefined
}
>
{state.replace('_', ' ')} <M>{counts[state]}</M>
</Chip>
))
}
export function Projects() {
const overview = useQuery({ queryKey: ['overview'], queryFn: api.overview, refetchInterval: 5000 })
const [selected, setSelected] = useState<string>()
const rows = useMemo(
() => derive(overview.data?.tasks || [], overview.data?.workers || []),
[overview.data],
)
const current = rows.find((r) => r.id === selected) || rows[0]
return (
<main className="page">
<div className="page-head">
<h1>Projects</h1>
<p>
Projects orchestra is holding work for, derived from the tasks it knows about. The
coordinator does not serve its project registry.
</p>
</div>
{overview.isError && (
<Panel>
<div className="panel-body">
<p className="gap-note">/v1/ui/overview failed: {String(overview.error)}</p>
</div>
</Panel>
)}
{!overview.data && !overview.isError && (
<Panel>
<div className="panel-body">
<p className="gap-note">reading /v1/ui/overview</p>
</div>
</Panel>
)}
{overview.data && rows.length === 0 && (
<Panel>
<Empty title="No project has work">
<p>
Every project here comes from a task. Orchestra holds{' '}
<M>{overview.data.tasks.length}</M> tasks, so nothing groups yet.
</p>
</Empty>
</Panel>
)}
{current && (
<div className="grid projects-split">
<Panel title="With work" count={rows.length}>
<div className="table-scroll">
<table className="table">
<thead>
<tr>
<th>Project</th>
<th>Tasks</th>
<th>Leased</th>
<th>Attention</th>
<th>Last session evidence</th>
<th>Workers</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr
key={row.id}
onClick={() => setSelected(row.id)}
style={{
cursor: 'pointer',
background: row.id === current.id ? 'var(--bg-2)' : undefined,
boxShadow:
row.id === current.id ? 'inset 3px 0 0 0 var(--accent)' : undefined,
}}
>
<td>
<M>{row.id}</M>
</td>
<td>
<M>{row.tasks.length}</M>
</td>
<td>
<M>{row.active}</M>
</td>
<td>
{row.attention ? (
<Chip tone="fault">
<M>{row.attention}</M>
</Chip>
) : (
<M>0</M>
)}
</td>
<td>
<M>{fmt(row.lastSeen) || '—'}</M>
</td>
<td>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
{row.workers.length === 0 ? (
<M>none</M>
) : (
row.workers.map((w) => (
<Chip key={w.id}>
<Dot health={w.online ? 'healthy' : 'error'} />
<M>{w.id}</M>
</Chip>
))
)}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Panel>
<div className="grid" style={{ alignContent: 'start' }}>
<Panel
title={current.id}
trailing={<Link to={`/tasks?project=${encodeURIComponent(current.id)}`}>Tasks </Link>}
>
<div className="panel-body" style={{ display: 'grid', gap: 12 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{stateChips(current.counts)}
</div>
<div>
<div className="label">Workers declaring this project</div>
<p style={{ margin: '4px 0 0', color: 'var(--text-mid)' }}>
{current.workers.length === 0
? 'No registered worker accepts it. Orchestra does not treat an omitted declaration as a wildcard, so nothing can lease this work.'
: 'Worker-declared, from the federation registry — not the coordinators project affinity.'}
</p>
{current.workers.map((w) => (
<div key={w.id} style={{ marginTop: 6 }}>
<Dot health={w.online ? 'healthy' : 'error'} /> <M>{w.id}</M>{' '}
<M>{w.address || ''}</M>
</div>
))}
</div>
</div>
</Panel>
<Panel title="Tasks" count={current.tasks.length}>
<div className="rows">
{current.tasks.slice(0, 8).map((task) => (
<Link key={task.id} className="row" to={`/tasks/${task.id}`}>
<span className="row-main">
<span className="row-title">{task.title || task.external_id || 'untitled'}</span>
<span className="row-sub">
<M>{task.id}</M>
</span>
</span>
<Chip
tone={
task.state === 'leased'
? 'accent'
: task.state === 'completed'
? 'done'
: task.state === 'needs_attention'
? 'warn'
: ATTENTION.includes(task.state)
? 'fault'
: undefined
}
>
{task.state.replace('_', ' ')}
</Chip>
</Link>
))}
</div>
{current.tasks.length > 8 && (
<div className="panel-body">
<Link to={`/tasks?project=${encodeURIComponent(current.id)}`}>
All <M>{current.tasks.length}</M> tasks
</Link>
</div>
)}
</Panel>
<Panel title="Project configuration">
<EndpointGap
path="/v1/ui/projects"
what="repo, remote, quality gate and verification policy live in the coordinator's config.jsonc and are not served over HTTP"
/>
</Panel>
</div>
</div>
)}
</main>
)
}