Files
orchestra/web/src/main.tsx
T
2026-07-29 23:31:25 +04:00

46 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React,{useEffect,useRef,useState} from 'react'
import {createRoot} from 'react-dom/client'
import {BrowserRouter,Link,NavLink,Route,Routes,useLocation,useNavigate,useParams} from 'react-router-dom'
import {QueryClient,QueryClientProvider,useMutation,useQuery,useQueryClient} from '@tanstack/react-query'
import {api} from './api/client'
import type {Action,BlockReason,Capture,Detail,Overview,PendingApproval,Task,TaskState} from './api/types'
import './style.css'
const client=new QueryClient()
const states:TaskState[]=['queued','leased','completed','failed']
const label:Record<TaskState,string>={queued:'Queued',leased:'In session',blocked:'Blocked',completed:'Complete',failed:'Failed'}
const blockLabel:Record<BlockReason,string>={lease_failure:'Lease failure',worker_offline:'Worker offline',lease_expired:'Expired lease',approval:'Approval needed',handoff_validation:'Handoff validation',operator_block:'Operator block',system_error:'System error',unknown:'Unknown'}
const actionLabel:Record<string,string>={handoff:'Request handoff',release:'Release task',complete:'Complete task',block:'Mark blocked'}
const date=(value?:string)=>value?new Date(value).toLocaleString():'—'
function CommandPalette({close}:{close:()=>void}){const nav=useNavigate(),qc=useQueryClient(),input=useRef<HTMLInputElement>(null),[term,setTerm]=useState('');useEffect(()=>{input.current?.focus()},[]);const choose=(id:string)=>{if(id==='board')nav('/');if(id==='workers')nav('/workers');if(id==='new'){nav('/');window.dispatchEvent(new Event('orchestra:new-task'))}if(id==='refresh')qc.invalidateQueries();close()};const commands=[['board','Go to dispatch board','G B'],['new','Create a new task','N'],['workers','View worker pool','G W'],['refresh','Refresh live data','R']] as const;const matches=commands.filter(c=>c[1].toLowerCase().includes(term.toLowerCase()));return <div className="palette-backdrop" onMouseDown={close}><section className="palette" role="dialog" aria-modal="true" aria-label="Command palette" onMouseDown={e=>e.stopPropagation()}><input ref={input} value={term} onChange={e=>setTerm(e.target.value)} onKeyDown={e=>{if(e.key==='Escape')close();if(e.key==='Enter'&&matches[0])choose(matches[0][0])}} placeholder="Find a command…" aria-label="Find a command"/><div className="palette-list">{matches.map(([id,name,key])=><button key={id} className="palette-command" onClick={()=>choose(id)}><span>{name}</span><kbd>{key}</kbd></button>)}{!matches.length&&<p className="palette-empty">No matching command.</p>}</div><p className="palette-foot"><kbd></kbd> run <kbd>esc</kbd> close</p></section></div>}
function Shell({children,onLogout}:{children:React.ReactNode;onLogout:()=>void}){const where=useLocation(),nav=useNavigate(),[palette,setPalette]=useState(false),[accountOpen,setAccountOpen]=useState(false);const overview=useQuery({queryKey:['overview'],queryFn:api.overview,refetchInterval:5000});const sessions=overview.data?.sessions?.length??0;useEffect(()=>{const key=(e:KeyboardEvent)=>{const editable=e.target instanceof HTMLInputElement||e.target instanceof HTMLTextAreaElement;if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==='k'){e.preventDefault();setPalette(true)}if(e.key==='n'&&!e.metaKey&&!e.ctrlKey&&!editable){nav('/');window.dispatchEvent(new Event('orchestra:new-task'))}if(e.key==='r'&&!e.metaKey&&!e.ctrlKey&&!editable)overview.refetch()};window.addEventListener('keydown',key);return()=>window.removeEventListener('keydown',key)},[nav,overview]);return <div className="app-shell" data-app="orchestra"><aside className="rail"><Link className="mark" to="/" aria-label="Orchestra home"><i className="branch-mark"/><span>OR</span></Link><nav className="rail-nav" aria-label="Primary navigation"><NavLink end className={({isActive})=>'rail-link '+(isActive?'active':'')} to="/"><span>Board</span></NavLink><NavLink className={({isActive})=>'rail-link '+(isActive?'active':'')} to="/workers"><span>Workers</span></NavLink></nav><span className="rail-footer">v0.1</span></aside><header className="topbar"><div className="topbar-title">Orchestra <small>{where.pathname==='/'?'dispatch board':where.pathname==='/workers'?'worker pool':'task record'}</small></div><button className="command" onClick={()=>setPalette(true)} aria-label="Open command palette"><span>Command</span><kbd> K</kbd></button><div className="readout"><span>SESSIONS <b>{sessions}</b></span><span>SYNC <b>5s</b></span></div><div className="account"><button className="account-button" onClick={()=>setAccountOpen(v=>!v)} aria-expanded={accountOpen}>Signed in <span className="status-dot"/></button>{accountOpen&&<div className="account-menu"><span>Browser session active</span><button onClick={onLogout}>Sign out</button></div>}</div></header>{children}{palette&&<CommandPalette close={()=>setPalette(false)}/>}</div>}
function taskExplanation(task:Task,overview:Overview){const session=(overview.sessions??[]).find(s=>s.capture?.task_id===task.id);if(task.state==='queued'){const online=(overview.workers??[]).filter(w=>w.online);return online.length?`Waiting for a worker to lease it · ${online.length} worker${online.length===1?'':'s'} online`:'No registered worker is currently reachable'}if(task.state==='leased'){if(session?.pending_approval)return 'Waiting for an operator approval';if(task.lease&&new Date(task.lease.until)<new Date())return 'Lease has expired; waiting for reconciliation';if(session?.blocker)return session.blocker;return session?.capture?'Harness session is active':'Lease is active; live capture is unavailable'}if(task.state==='blocked'){const evidence=task.last_session;const observed=evidence?.captured_at||evidence?.checked_at;const suffix=evidence?` · ${evidence.source||'unknown'} observed ${date(observed)}`:'';return (task.blocker||'Unknown — legacy record has no retained blocker or pane evidence')+suffix}if(task.state==='failed')return 'The task needs review before it can be retried';return 'Terminal task record'}
function TaskCard({task,overview}:{task:Task;overview:Overview}){return <Link className="card" to={'/tasks/'+task.id}><b>{task.title||task.id}</b><small><span>{task.project}</span><span className="machine">{task.id.slice(-5)}</span></small><em>{taskExplanation(task,overview)}</em></Link>}
function Board({tasks,overview}:{tasks:Task[];overview:Overview}){const blocked=tasks.filter(t=>t.state==='blocked').sort((a,b)=>new Date(a.blocked_at||0).getTime()-new Date(b.blocked_at||0).getTime());const groups=new Map<BlockReason,Task[]>;for(const task of blocked){const reason=task.block_reason||'unknown';groups.set(reason,[...(groups.get(reason)||[]),task])}return <><div className="board">{states.map(state=>{const lane=tasks.filter(t=>t.state===state);return <section key={state}><div className="lane-head"><span>{label[state]}</span><span className="lane-count">{lane.length}</span></div>{lane.map(t=><TaskCard task={t} overview={overview} key={t.id}/>)}{!lane.length&&<p className="empty-lane">Nothing here. New work will appear in this branch.</p>}</section>})}</div>{blocked.length>0&&<section className="blocked-groups" aria-label="Blocked work by diagnosis"><div className="section-title"><div><h2>Needs attention</h2><p>Blocked work is grouped by diagnosis and ordered oldest first.</p></div><span className="machine">{blocked.length} blocked</span></div><div className="blocked-grid">{[...groups.entries()].map(([reason,group])=><section key={reason}><div className="lane-head"><span>{blockLabel[reason]}</span><span className="lane-count">{group.length}</span></div>{group.map(task=><TaskCard task={task} overview={overview} key={task.id}/>)}</section>)}</div></section>}</>}
function OverviewPage(){const q=useQuery({queryKey:['overview'],queryFn:api.overview,refetchInterval:5000}),[createOpen,setCreateOpen]=useState(false);useEffect(()=>{const open=()=>setCreateOpen(true);window.addEventListener('orchestra:new-task',open);return()=>window.removeEventListener('orchestra:new-task',open)},[]);if(q.isLoading)return <main className="page loading"><p className="eyebrow">Dispatch board</p><p>Fetching queue state from the coordinator</p></main>;if(q.error)return <main className="page error" role="alert">Queue unavailable: {String(q.error)}</main>;const d=q.data!,sessions=d.sessions??[],active=d.tasks.filter(t=>t.state==='leased').length,attention=d.tasks.filter(t=>t.state==='blocked'||t.state==='failed').length,approvals=new Set(sessions.filter(s=>s.pending_approval).map(s=>s.capture?.task_id));return <main className="page"><div className="page-heading"><div><p className="eyebrow">Agent dispatch</p><h1>Keep the work moving.</h1></div><div className="heading-actions"><p>Live task state across every connected harness. The board refreshes every five seconds.</p><button onClick={()=>setCreateOpen(true)}>New task <kbd>N</kbd></button></div></div><div className="status-strip" aria-label="Queue summary"><div><span>Tasks</span><b>{d.tasks.length}</b></div><div><span>In session</span><b>{active}</b></div><div className={attention?'attention':''}><span>Needs attention</span><b>{attention}</b></div><div className={approvals.size?'attention':''}><span>Awaiting approval</span><b>{approvals.size}</b></div></div><div className="section-title"><div><h2>Task flow</h2><p>Each card states the current reason it is waiting or running.</p></div><span className="machine" aria-live="polite">SYNCED {new Date(d.updated_at).toLocaleTimeString()}</span></div><Board tasks={d.tasks} overview={d}/>{createOpen&&<Create overview={d} close={()=>setCreateOpen(false)}/>}</main>}
function Create({overview,close}:{overview:Overview;close:()=>void}){const qc=useQueryClient(),nav=useNavigate(),first=useRef<HTMLInputElement>(null),[title,setTitle]=useState(''),[description,setDescription]=useState(''),[project,setProject]=useState(''),[capability,setCapability]=useState(''),[advanced,setAdvanced]=useState(''),[formError,setFormError]=useState('');const projects=[...new Set(overview.tasks.map(t=>t.project).filter(Boolean))];const online=overview.workers.filter(w=>w.online);useEffect(()=>{first.current?.focus()},[]);const m=useMutation({mutationFn:()=>{let extra:Record<string,unknown>={};if(advanced.trim()){try{extra=JSON.parse(advanced)}catch{return Promise.reject(new Error('Additional fields must be valid JSON.'))}}return api.create({...extra,source:'web',external_id:crypto.randomUUID(),project,title,description,capability:capability.split(',').map(x=>x.trim()).filter(Boolean)})},onSuccess:(e:any)=>{qc.invalidateQueries({queryKey:['overview']});nav('/tasks/'+e.task_id)}});const submit=(e:React.FormEvent)=>{e.preventDefault();setFormError('');if(title.trim().length<3){setFormError('Give the task a title of at least three characters.');return}if(!project.trim()){setFormError('Choose an existing project or enter its exact project ID.');return}if(description.trim().length<10){setFormError('Add enough immutable instructions for an agent to act safely.');return}m.mutate()};return <div className="modal-backdrop" onMouseDown={close}><section className="create modal" role="dialog" aria-modal="true" aria-labelledby="new-task-title" onMouseDown={e=>e.stopPropagation()}><div className="modal-heading"><div><p className="eyebrow">Dispatch new work</p><h2 id="new-task-title">New task</h2></div><button className="icon-button" onClick={close} aria-label="Close task creation">×</button></div><p>Give the harness a clear outcome and immutable operating instructions.</p><div className={'eligibility '+(!online.length?'warning':'')}><b>{online.length?`${online.length} worker${online.length===1?'':'s'} currently reachable`:'No workers are currently reachable'}</b><span>{online.length?'Orchestra will choose an eligible worker when the task is leased.':'This task will remain queued until a matching worker reconnects.'}</span></div><form onSubmit={submit} noValidate><label>Task title<input ref={first} required value={title} onChange={e=>setTitle(e.target.value)} placeholder="e.g. Add the health endpoint" autoComplete="off"/></label><label>Project <small>Existing projects are suggested; project IDs remain explicit.</small><input required list="projects" value={project} onChange={e=>setProject(e.target.value)} placeholder="Choose or enter a project" autoComplete="off"/><datalist id="projects">{projects.map(p=><option value={p} key={p}/>)}</datalist></label><label>Capabilities <small>Optional, comma-separated. Only use registered capability names.</small><input value={capability} onChange={e=>setCapability(e.target.value)} placeholder="go, docker" autoComplete="off"/></label><label>Immutable instructions<textarea required value={description} onChange={e=>setDescription(e.target.value)} placeholder="Describe the outcome, constraints, and evidence required."/></label><details><summary>Additional accepted task fields</summary><textarea aria-label="Additional task fields JSON" placeholder={'{"parent":"…","inherent_priority":1,"due":"2026-07-30T12:00:00Z"}'} value={advanced} onChange={e=>setAdvanced(e.target.value)}/></details><div className="modal-actions"><button type="button" className="quiet-button" onClick={close}>Cancel</button><button disabled={m.isPending}>{m.isPending?'Creating task…':'Create task'}</button></div>{(formError||m.error)&&<span className="error" role="alert">{formError||String(m.error)}</span>}</form></section></div>}
function Approval({approval,onAction,pending,notice}:{approval:PendingApproval;onAction:(a:string)=>void;pending:boolean;notice?:string}){const ref=useRef<HTMLDivElement>(null),grant=approval.kind!=='unknown',deny=approval.kind==='shell';useEffect(()=>{const node=ref.current;if(!node)return;node.querySelector<HTMLButtonElement>('button:not(:disabled)')?.focus();const trap=(e:KeyboardEvent)=>{if(e.key!=='Tab')return;const focus=[...node.querySelectorAll<HTMLElement>('button:not(:disabled)')],i=focus.indexOf(document.activeElement as HTMLElement);if(e.shiftKey&&i<=0){e.preventDefault();focus.at(-1)?.focus()}else if(!e.shiftKey&&i===focus.length-1){e.preventDefault();focus[0].focus()}};node.addEventListener('keydown',trap);return()=>node.removeEventListener('keydown',trap)},[]);const input=approval.command||approval.diff||'The harness prompt could not be parsed safely.';return <div className="approval-backdrop"><section ref={ref} className="approval" role="dialog" aria-modal="true" aria-labelledby="approval-title"><p className="eyebrow">Harness gate</p><h2 id="approval-title">{approval.summary}</h2><div className="approval-summary"><span>Pane <b>{approval.pane_id}</b></span><span>Seen <b>{date(approval.detected_at)}</b></span><span>Revision <b>{approval.capture_revision}</b></span></div><p>The exact input below is the permission request currently shown by the harness.</p><pre>{input}</pre>{notice&&<p className="notice" role="status">{notice}</p>}{approval.kind==='opencode_once'&&<p>Approving sends Enter to OpenCodes displayed <b>Allow once</b> selection. Reject is unavailable because the selected position cannot be verified from the capture.</p>}{!grant&&<p role="alert">This prompt cannot be safely executed because its exact confirmation is unknown.</p>}<div><button disabled={!grant||pending} onClick={()=>onAction('grant_approval')}>{pending?'Sending…':approval.kind==='opencode_once'?'Allow once':'Approve'}</button><button disabled={!deny||pending} title={deny?'':'This selector does not expose a verifiable reject position.'} onClick={()=>onAction('deny_approval')}>Reject</button></div></section></div>}
const actionHelp:Record<string,string>={handoff:'Ask the active harness to prepare a safe handoff. The task stays leased until the handoff is accepted.',release:'Release the current lease. The task may become eligible for another worker after routing.',block:'Stop normal routing and record a concrete blocker for the next operator.',complete:'Complete the task with a durable report. This is a terminal action.'}
function Lifecycle({action,mutate,pending}:{action:Action;mutate:(action:string,body?:object)=>void;pending:boolean}){const [value,setValue]=useState(''),[confirm,setConfirm]=useState(false);const submit=(actionID:string,body?:object)=>{if(['release','block','complete'].includes(actionID)&&!confirm){setConfirm(true);return}mutate(actionID,body)};if(action.id==='handoff')return <div className="action-row"><div><b>{actionLabel[action.id]}</b><span>{actionHelp[action.id]}</span></div><button disabled={!action.enabled||pending} title={action.reason} onClick={()=>mutate('handoff')}>Request handoff</button></div>;if(action.id==='complete')return <form className="action-form" onSubmit={async e=>{e.preventDefault();try{const ref=await api.upload(value);submit('complete',{report_ref:ref,receipt:{source:'web',completed_at:new Date().toISOString()}})}catch(err){alert(String(err))}}}><div><b>Complete task</b><span>{confirm?'Confirming will terminally complete this task. Submit again to continue.':actionHelp.complete}</span></div><textarea required placeholder="Completion report, stored as evidence" value={value} onChange={e=>{setValue(e.target.value);setConfirm(false)}}/><button disabled={!action.enabled||pending}>{confirm?'Confirm completion':'Complete task'}</button></form>;const field=action.id==='block'?'blocker':'reason';return <form className="action-form compact" onSubmit={e=>{e.preventDefault();submit(action.id,{[field]:value})}}><div><b>{actionLabel[action.id]||action.id}</b><span>{confirm?`Confirming will ${action.id==='release'?'release this lease':'block this task'}. Submit again to continue.`:actionHelp[action.id]||action.reason}</span></div><input required placeholder={field==='blocker'?'What is blocking progress?':'Reason for release'} value={value} onChange={e=>{setValue(e.target.value);setConfirm(false)}}/><button disabled={!action.enabled||pending} title={action.reason||action.needs?.join(',')}>{confirm?'Confirm':'Continue'}</button></form>}
function CaptureView({capture,blocker}:{capture?:Capture;blocker?:string}){const [expanded,setExpanded]=useState(false),[filter,setFilter]=useState('');if(!capture)return <div className="capture-empty"><b>Live capture unavailable</b><span>{blocker||'The session has not published a recent pane capture.'}</span></div>;const lines=capture.text.split('\n'),visible=filter?lines.filter(l=>l.toLowerCase().includes(filter.toLowerCase())):lines;const preview=expanded?visible:visible.slice(-18);return <section className="capture"><div className="capture-head"><div><h2>Live capture</h2><span>{capture.source} · updated {date(capture.at)} · {lines.length} lines{capture.truncated?' · truncated':''}</span></div><button className="quiet-button" onClick={()=>setExpanded(v=>!v)}>{expanded?'Show recent':'Expand'}</button></div><input className="capture-filter" value={filter} onChange={e=>setFilter(e.target.value)} placeholder="Filter visible capture" aria-label="Filter live capture"/><pre>{preview.join('\n')||'No lines match this filter.'}</pre>{!expanded&&visible.length>preview.length&&<p className="capture-more">Showing the most recent {preview.length} matching lines.</p>}</section>}
function TaskDiagnosis({detail}:{detail:Detail}){const evidence=detail.task.last_session,approval=detail.session?.pending_approval,action=detail.actions.find(a=>a.enabled),observed=evidence?.captured_at||evidence?.checked_at;const next=approval?{title:'Review the permission request',copy:'This harness is paused at a permission boundary. Approve only the exact request shown.'}:action?{title:actionLabel[action.id]||action.id,copy:actionHelp[action.id]||'This action is currently safe to submit.'}:{title:'No safe lifecycle action is available',copy:'Review the retained diagnosis and pane evidence. Unavailable actions explain their requirements below.'};return <section className={'diagnosis state-'+detail.task.state} aria-label="Task diagnosis"><div className="diagnosis-heading"><div><p className="eyebrow">Current diagnosis</p><h2>{detail.task.state==='blocked'?blockLabel[detail.task.block_reason||'unknown']:label[detail.task.state]}</h2><p>{taskDetailExplanation(detail)}</p></div><span className="task-state">{label[detail.task.state]}</span></div><dl className="diagnosis-evidence"><div><dt>Last activity</dt><dd>{date(observed||detail.session?.capture?.at)}</dd></div><div><dt>Pane state</dt><dd>{evidence?.pane_state||detail.session?.agent_status||'unknown'}</dd></div><div><dt>Pane</dt><dd className="machine">{evidence?.pane_id||detail.session?.pane_id||'—'}</dd></div><div><dt>Source</dt><dd>{evidence?.source||detail.session?.capture?.source||'unknown'}</dd></div></dl><div className="next-action"><div><span>Next safe action</span><b>{next.title}</b><p>{next.copy}</p></div>{detail.session?.lease_until&&<small>Lease ends {date(detail.session.lease_until)}</small>}</div></section>}
function TaskDetail(){const {taskID=''}=useParams(),qc=useQueryClient(),[notice,setNotice]=useState('');const q=useQuery({queryKey:['task',taskID],queryFn:()=>api.detail(taskID),refetchInterval:3000});const m=useMutation({mutationFn:({action,body}:{action:string;body?:object})=>api.action(taskID,action,body),onSuccess:(result:any,vars)=>{setNotice(vars.action.includes('approval')?'Approval request has been queued; waiting for the worker acknowledgement.':'Action recorded.');qc.invalidateQueries({queryKey:['task',taskID]});qc.invalidateQueries({queryKey:['overview']})}});if(q.isLoading)return <main className="page loading">Fetching task record</main>;if(q.error)return <main className="page error" role="alert">Task unavailable: {String(q.error)}</main>;const d=q.data!,events=d.events??[],available=d.actions.filter(a=>a.enabled),unavailable=d.actions.filter(a=>!a.enabled),evidence=d.task.last_session;return <main className="page"><Link className="back" to="/"> Dispatch board</Link><div className="page-heading"><div><p className="eyebrow">Task record</p><h1>{d.task.title||d.task.id}</h1></div><p className="machine">{d.task.id}</p></div><TaskDiagnosis detail={d}/>{d.session?.pending_approval&&<Approval approval={d.session.pending_approval} pending={m.isPending} notice={notice} onAction={a=>m.mutate({action:a})}/>}<div className="detail-grid"><div className="surface"><h2>Instructions</h2><p className="task-description">{d.task.description||'No immutable instructions were recorded.'}</p><CaptureView capture={d.session?.capture} blocker={d.session?.blocker||d.task.blocker}/>{available.length>0&&<><h2 className="actions-title">Available actions</h2><p className="section-copy">Only actions whose server-side safety requirements are met are shown here.</p><div className="actions">{available.map(a=><Lifecycle key={a.id} action={a} pending={m.isPending} mutate={(action,body)=>m.mutate({action,body})}/>)}</div></>}{unavailable.length>0&&<details className="unavailable-actions"><summary>Unavailable actions ({unavailable.length})</summary><p>These controls are disabled until their stated server-side requirements are met.</p><div className="actions">{unavailable.map(a=><Lifecycle key={a.id} action={a} pending={m.isPending} mutate={(action,body)=>m.mutate({action,body})}/>)}</div></details>}{m.error&&<p className="error" role="alert">{String(m.error)}</p>}{notice&&!d.session?.pending_approval&&<p className="notice" role="status">{notice}</p>}</div><aside className="surface"><h2>Session evidence</h2><ul className="meta"><li><span>Project</span><span className="machine">{d.task.project}</span></li><li><span>Harness</span><span className="machine">{evidence?.harness_id||d.session?.harness_id||'—'}</span></li><li><span>Pane</span><span className="machine">{evidence?.pane_id||d.session?.pane_id||'—'}</span></li><li><span>Observed</span><span className="machine">{date(evidence?.captured_at||evidence?.checked_at)}</span></li><li><span>Lease ends</span><span className="machine">{date(d.session?.lease_until)}</span></li></ul>{d.handoff_ref&&<p><Link className="back" to={'/artifacts/'+d.handoff_ref}>View handoff </Link></p>}{d.report_ref&&<p><Link className="back" to={'/artifacts/'+d.report_ref}>View report </Link></p>}</aside></div><div className="surface timeline-surface"><div className="timeline-heading"><div><h2>Timeline</h2><p>Recorded operator and lifecycle decisions.</p></div><span>{events.length} events</span></div><ol className="timeline">{events.map(e=><li key={e.id}><span>{e.type}</span><time>{date(e.at)}</time></li>)}</ol></div></main>}
function taskDetailExplanation(d:Detail){if(d.session?.pending_approval)return 'This harness is paused at a permission boundary. Review the exact request before allowing it.';if(d.task.state==='leased')return d.session?.capture?'The harness is active and publishing a recent capture.':'The task is leased, but its live capture is unavailable.';if(d.task.state==='blocked')return d.task.blocker||'Unknown — legacy record has no retained blocker or pane evidence.';return label[d.task.state]+' task record.'}
function Workers(){const q=useQuery({queryKey:['overview'],queryFn:api.overview,refetchInterval:5000});if(q.isLoading)return <main className="page loading">Fetching worker heartbeats</main>;if(q.error)return <main className="page error" role="alert">Worker pool unavailable: {String(q.error)}</main>;const d=q.data!,workers=d.workers;const herdr=(status?:string)=>status==='reachable'?'local herdr reachable':status==='unreachable'?'local herdr unreachable':'local herdr not yet checked';return <main className="page"><Link className="back" to="/"> Dispatch board</Link><div className="page-heading"><div><p className="eyebrow">Worker pool</p><h1>Available capacity.</h1></div><p>{workers.filter(w=>w.online).length} of {workers.length} registered workers are reachable right now.</p></div>{workers.length?<><p className="worker-note">Heartbeat proves the worker can reach Orchestra. Local herdr status is separately reported by that worker; it is never inferred from legacy coordinator TCP probes.</p><table><thead><tr><th>Worker</th><th>Heartbeat</th><th>Local herdr</th><th>Active work</th><th>Last error</th><th>Last heartbeat</th></tr></thead><tbody>{workers.map(w=><tr key={w.id}><td>{w.id}</td><td className={w.online?'online':'offline'}><span className="status-dot"/> {w.online?'reachable':'overdue'}</td><td className={w.health?.herdr_status==='reachable'?'online':w.health?.herdr_status==='unreachable'?'offline':''}>{herdr(w.health?.herdr_status)}<small>{date(w.health?.checked_at)}</small></td><td>{w.health?.active_task_id?<><span>{w.health.active_task_id}</span><small>{w.health.active_pane_id||'pane unknown'}</small></>:'idle'}</td><td>{w.health?.last_error?<><span>{w.health.last_error}</span><small>{date(w.health.error_at)}</small></>:'—'}</td><td>{date(w.last_seen)}</td></tr>)}</tbody></table></>:<section className="empty-panel"><i className="branch-mark"/><h2>No workers registered</h2><p>Connect a worker to begin leasing queued tasks.</p></section>}</main>}
function Artifact(){const {ref=''}=useParams();const q=useQuery({queryKey:['artifact',ref],queryFn:()=>api.artifact(ref)});return <main className="page"><Link className="back" to="/"> Dispatch board</Link><div className="page-heading"><div><p className="eyebrow">Evidence artifact</p><h1>Recorded output.</h1></div><p className="machine">{ref}</p></div>{q.isLoading?<p className="loading">Retrieving artifact</p>:q.error?<p className="error" role="alert">{String(q.error)}</p>:<pre>{q.data}</pre>}</main>}
function Login({onAuthenticated,message}:{onAuthenticated:()=>void;message?:string}){const [token,setToken]=useState(''),[error,setError]=useState(''),[pending,setPending]=useState(false);const submit=async(e:React.FormEvent)=>{e.preventDefault();setError('');setPending(true);try{await api.login(token);setToken('');onAuthenticated()}catch(err){setError(String(err))}finally{setPending(false)}};return <main className="login-page"><section className="login-card"><p className="eyebrow">Orchestra control plane</p><h1>Sign in</h1><p>Enter the Web token to open a secure browser session.</p>{message&&<p className="session-message" role="status">{message}</p>}<form onSubmit={submit}><label>Web token<input type="password" autoComplete="current-password" autoFocus value={token} onChange={e=>setToken(e.target.value)} required/></label><button disabled={pending}>{pending?'Signing in…':'Sign in'}</button>{error&&<span className="error" role="alert">{error}</span>}</form></section></main>}
function RoutesApp({onLogout}:{onLogout:()=>void}){return <Shell onLogout={onLogout}><Routes><Route path="/" element={<OverviewPage/>}/><Route path="/tasks/:taskID" element={<TaskDetail/>}/><Route path="/workers" element={<Workers/>}/><Route path="/artifacts/:ref" element={<Artifact/>}/></Routes></Shell>}
function App(){const [ready,setReady]=useState(false),[checking,setChecking]=useState(true),[message,setMessage]=useState('');useEffect(()=>{const unauth=()=>{client.clear();setMessage('Your browser session expired. Sign in again to continue.');setReady(false);setChecking(false)};window.addEventListener('orchestra:unauthorized',unauth);api.overview().then(()=>setReady(true)).catch(()=>setReady(false)).finally(()=>setChecking(false));return()=>window.removeEventListener('orchestra:unauthorized',unauth)},[]);const logout=async()=>{await api.logout();client.clear();setMessage('You have signed out.');setReady(false)};if(checking)return <main className="login-page">Checking session</main>;return ready?<RoutesApp onLogout={logout}/>:<Login message={message} onAuthenticated={()=>{client.clear();setMessage('');setReady(true)}}/>}
createRoot(document.getElementById('root')!).render(<React.StrictMode><QueryClientProvider client={client}><BrowserRouter><App/></BrowserRouter></QueryClientProvider></React.StrictMode>)