import { Link } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' import { api } from '../api/client' import type { BlockReason, Overview, Session, Task, Worker } from '../api/types' import { Chip, Dot, Empty, EndpointGap, M, Panel, PhasePath } from '../components/Primitives' import { Icon } from '../components/Icon' import './Dashboard.css' /* The ace-fca phase lives on the task as work_phase; the shared Task type has not caught up with the Go domain yet, so it is read through a narrow cast rather than invented or renamed. */ const phaseOf = (t: Task) => (t as Task & { work_phase?: string }).work_phase const now = () => Date.now() /** "18s ago" / "2m ago" / "1h ago" — a machine value, so it renders mono. */ function ago(at?: string) { // Go marshals a zero time.Time as "0001-01-01T00:00:00Z" and omitempty does // not omit a struct, so an absent timestamp arrives populated-looking. Left // unguarded this rendered "739855d ago", which reads as data. if (!at || at.startsWith('0001-01-01')) return undefined const s = Math.max(0, Math.round((now() - Date.parse(at)) / 1000)) if (!Number.isFinite(s)) return undefined if (s < 60) return `${s}s ago` if (s < 3600) return `${Math.floor(s / 60)}m ago` if (s < 86400) return `${Math.floor(s / 3600)}h ago` return `${Math.floor(s / 86400)}d ago` } /** Time remaining on a lease as mm:ss, or "expired" when it already ran out. */ function until(at?: string) { if (!at) return undefined const s = Math.round((Date.parse(at) - now()) / 1000) if (!Number.isFinite(s)) return undefined if (s <= 0) return 'expired' const m = Math.floor(s / 60) return `${String(m).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}` } const shortId = (id: string) => (id.length > 14 ? `${id.slice(0, 6)}…${id.slice(-4)}` : id) /* Why an item is on the operator's desk. Every line is derived from a real block_reason or state — none of it is decorative copy. */ const attentionReason: Record = { trajectory_gate: 'A trajectory gate is waiting for your steering.', human_decision: 'The agent asked for a decision it may not make itself.', operator_required: 'Orchestra cannot proceed without an operator action.', approval: 'A tool or edit approval is pending.', handoff_validation: 'The handoff failed validation and was not accepted.', lease_failure: 'The lease could not be established.', lease_expired: 'The lease expired before the agent finished.', worker_offline: 'The worker holding this task went offline.', system_error: 'Orchestra hit an error it could not retry past.', operator_block: 'You blocked this task.', unknown: 'Blocked for a reason orchestra could not classify.', } const stateReason: Partial> = { in_review: 'A pull request is waiting on review.', failed: 'The task failed and will not retry on its own.', needs_attention: 'Orchestra flagged this task for you.', blocked: 'The task is blocked.', } function reasonFor(t: Task) { if (t.block_reason && attentionReason[t.block_reason]) return attentionReason[t.block_reason] return stateReason[t.state] ?? 'Waiting on you.' } const needsOperator = (t: Task) => t.state === 'needs_attention' || t.state === 'blocked' || t.state === 'failed' || t.state === 'in_review' /** Sessions carry no task id of their own unless a capture is attached, so * they are matched on the pane the task last held. */ function sessionFor(t: Task, sessions: Session[]) { return sessions.find( (s) => s.capture?.task_id === t.id || (t.last_pane_id !== undefined && s.pane_id === t.last_pane_id) || (t.lease !== undefined && s.harness_id === t.lease.harness_id), ) } function attentionIcon(t: Task) { if (t.state === 'in_review') return 'review' if (t.block_reason === 'trajectory_gate') return 'fork' if (t.block_reason === 'human_decision' || t.block_reason === 'approval') return 'decision' return 'alert' } function Attention({ tasks }: { tasks: Task[] }) { return ( View all} > {tasks.length === 0 ? (

Blocked tasks, trajectory gates and open reviews land here.

) : (
{tasks.map((t) => ( {t.title || 'Untitled task'} {reasonFor(t)} {t.title ? t.project : t.id} {shortId(t.id)} {phaseOf(t) && ( <> {' · '} {phaseOf(t)} )} {t.blocked_at && ( {ago(t.blocked_at)} )} {t.block_reason ?? t.state} ))}
)}
) } function Running({ tasks, sessions }: { tasks: Task[]; sessions: Session[] }) { return ( View all tasks} > {tasks.length === 0 ? (

Leased sessions appear here with their phase, worker and lease clock.

) : (
{tasks.map((t) => { const s = sessionFor(t, sessions) const left = until(t.lease?.until) return ( ) })}
Task Phase Worker / harness Last progress Lease ends in Attempt
{t.title || 'Untitled task'} {shortId(t.id)}
{phaseOf(t) ?? 'unknown'}
{t.lease?.harness_id ?? '—'} {s?.pane_id ?? t.last_pane_id ?? 'pane unknown'} {s?.capture?.at ? ( <> {ago(s.capture.at)} capture r{s.capture.revision} ) : ( no capture yet )} {left ?? '—'} {s?.agent_status && {s.agent_status}} {t.attempt ?? 1}
)}
) } function Capacity({ workers, tasks }: { workers: Worker[]; tasks: Task[] }) { const online = workers.filter((w) => w.online).length const reachable = workers.filter((w) => w.health.herdr_status === 'reachable').length const unreachable = workers.filter((w) => w.health.herdr_status === 'unreachable').length const leased = tasks.filter((t) => t.lease) const expired = leased.filter((t) => until(t.lease?.until) === 'expired').length const erroring = workers.filter((w) => w.health.last_error) return ( View workers}>
Workers online {online} / {workers.length} {workers.reduce((n, w) => n + w.capacity, 0)} total capacity
Active leases {leased.length} {expired} past their lease end
Herdr backends {reachable} / {workers.length} {unreachable} unreachable, from worker health
{workers.length === 0 ? (

No worker has ever registered.

) : ( workers.map((w) => ( {w.id} {w.health.backend ?? 'backend unknown'} seen {ago(w.last_seen) ?? '—'} {w.health.active_task_id && ( {shortId(w.health.active_task_id)} )} {w.build?.revision && {w.build.revision.slice(0, 7)}} )) )}
{erroring.length > 0 && (
{erroring.map((w) => (

{w.id}{' '} {w.health.last_error}{' '} {ago(w.health.error_at) ?? ''}

))}
)}
) } export function Dashboard() { const overview = useQuery({ queryKey: ['overview'], queryFn: api.overview, refetchInterval: 5000, }) const workers = useQuery({ queryKey: ['workers'], queryFn: api.workers, refetchInterval: 5000, }) const tasks = overview.data?.tasks ?? [] const sessions = overview.data?.sessions ?? [] // Workers come from the federation registry; the overview snapshot is the // fallback when that call has not landed or is failing. const workerList = workers.data ?? overview.data?.workers ?? [] return (

Dashboard

What needs you, what is running, and whether the system is healthy.

{overview.data ? ( <> read {new Date(overview.data.updated_at).toISOString().slice(11, 19)} UTC ) : overview.isError ? ( overview unreachable — {String(overview.error)} ) : ( 'reading the first snapshot' )}
t.state === 'leased')} sessions={sessions} />
) }