// Kanban board, cards, and the right-side detail panel
// ISO timestamp -> "just now" / "5m ago" / "3h ago" / "2d ago"
function relTime(iso) {
if (!iso) return '';
const t = new Date(iso).getTime();
if (isNaN(t)) return iso;
const s = Math.max(0, Math.floor((Date.now() - t) / 1000));
if (s < 10) return 'just now';
if (s < 60) return `${s}s ago`;
const m = Math.floor(s / 60);
if (m < 60) return `${m}m ago`;
const h = Math.floor(m / 60);
if (h < 24) return `${h}h ago`;
return `${Math.floor(h / 24)}d ago`;
}
function TaskCard({ task, project, agent, selected, onClick, animState, actions }) {
const showApprove = task.column === 'review';
const showRollback = task.column === 'done';
const showRetry = task.column === 'failed';
const act = actions || {};
project = project || { color: 'var(--c-text-3)', name: task.project };
const typeLabel = {
'module-install': 'Module Install',
'update': 'Update',
'agent-task': 'Agent Task',
'system-task': 'System Task',
}[task.type];
return (
{task.title}
{project.name}
{relTime(task.updated)}
{task.agent}
{task.running ? (
<>
{Math.round(task.progress)}%
>
) : task.column === 'failed' ? (
halted
) : task.column === 'done' ? (
) : null}
db:
{task.affectsDb ? 'yes' : 'no'}
approval:
{task.requiresApproval ? 'required' : 'no'}
{task.version}
{task.column === 'failed' && task.error && (
{task.error}
)}
e.stopPropagation()}>
{task.column === 'backlog' && act.transition && (
)}
{task.column === 'ready' && act.transition && (
)}
{task.column === 'running' && act.transition && (
)}
{showRetry && act.transition && }
{showApprove && act.approve && }
{showRollback && }
);
}
function Column({ column, tasks, projectsById, agentsById, selectedId, onSelect, animMap, actions }) {
return (
{column.name}
{tasks.length}
{tasks.length === 0 ? (
no tasks
) : tasks.map(t => (
onSelect(t.id)}
actions={actions}
/>
))}
);
}
function Board({ tasks, onSelect, selectedId, animMap, actions }) {
const { COLUMNS, PROJECTS, AGENTS } = window.ORC_DATA;
const projectsById = Object.fromEntries((PROJECTS || []).map(p => [p.id, p]));
const agentsById = Object.fromEntries((AGENTS || []).map(a => [a.id, a]));
return (
{(COLUMNS || []).map(col => (
t.column === col.id)}
projectsById={projectsById}
agentsById={agentsById}
selectedId={selectedId}
onSelect={onSelect}
animMap={animMap}
actions={actions}
/>
))}
);
}
/* ---------------- Detail panel ---------------- */
const SAMPLE_LOGS = [
{ time: '00:14:01.221', level: 'info', msg: 'Acquired lock on tenant=helio.prod' },
{ time: '00:14:01.498', level: 'info', msg: 'Resolved migration plan: 3 ops, ~14m est.' },
{ time: '00:14:02.012', level: 'info', msg: 'Snapshotted users (12,438,221 rows) → s3://hl-bk/2026-05-04T00-14' },
{ time: '00:14:02.880', level: 'ok', msg: 'Snapshot integrity verified (sha256 matches manifest)' },
{ time: '00:14:08.402', level: 'info', msg: 'CREATE TABLE users_new PARTITION BY HASH (org_id) PARTITIONS 12' },
{ time: '00:14:09.140', level: 'info', msg: 'Backfill batch 1/12 → 1,036,518 rows in 642ms' },
{ time: '00:14:09.802', level: 'warn', msg: 'Slow rebuild on idx_users_email (est. 4m, ~3x baseline)' },
{ time: '00:14:14.011', level: 'info', msg: 'Backfill batch 4/12 → 1,041,002 rows in 681ms' },
{ time: '00:14:18.553', level: 'info', msg: 'Switching read traffic to users_new in shadow mode' },
{ time: '00:14:18.640', level: 'ok', msg: 'Shadow read parity: 100.000% across 50k probe queries' },
];
const SAMPLE_DIFF = [
{ kind: 'hunk', n: '', mark: '', text: '@@ migrations/2026_04_28_partition_users.sql' },
{ kind: 'ctx', n: '12', mark: ' ', text: '-- Phase 2: cutover to partitioned schema' },
{ kind: 'del', n: '13', mark: '-', text: 'CREATE TABLE users (' },
{ kind: 'del', n: '14', mark: '-', text: ' id UUID PRIMARY KEY,' },
{ kind: 'del', n: '15', mark: '-', text: ' org_id UUID NOT NULL,' },
{ kind: 'add', n: '13', mark: '+', text: 'CREATE TABLE users (' },
{ kind: 'add', n: '14', mark: '+', text: ' id UUID NOT NULL,' },
{ kind: 'add', n: '15', mark: '+', text: ' org_id UUID NOT NULL,' },
{ kind: 'add', n: '16', mark: '+', text: ' PRIMARY KEY (org_id, id)' },
{ kind: 'add', n: '17', mark: '+', text: ') PARTITION BY HASH (org_id) PARTITIONS 12;' },
{ kind: 'ctx', n: '18', mark: ' ', text: '' },
{ kind: 'ctx', n: '19', mark: ' ', text: 'CREATE INDEX idx_users_email ON users (email);' },
{ kind: 'del', n: '20', mark: '-', text: 'CREATE INDEX idx_users_org ON users (org_id);' },
{ kind: 'add', n: '20', mark: '+', text: '-- idx_users_org redundant after partition key' },
];
const SAMPLE_STEPS = [
{ state: 'done', title: 'Plan validated', meta: ['planner-agent', '0.4s'] },
{ state: 'done', title: 'Snapshot created', meta: ['migrator-agent', '0.9s', '1.2 GB'] },
{ state: 'done', title: 'Schema applied (shadow)', meta: ['migrator-agent', '5.3s'] },
{ state: 'running', title: 'Backfilling 12 partitions', meta: ['migrator-agent', '7/12 shards · 4m 12s'] },
{ state: 'pending', title: 'Cutover read traffic', meta: ['sentinel-agent', 'queued'] },
{ state: 'pending', title: 'Cutover write traffic', meta: ['sentinel-agent', 'queued'] },
{ state: 'pending', title: 'Drop legacy table', meta: ['migrator-agent', 'awaits approval'] },
];
function Logs() {
return (
{SAMPLE_LOGS.map((l, i) => (
{l.time}
{l.level}
{l.msg}
))}
);
}
function Diff() {
return (
migrations/2026_04_28_partition_users.sql
+6
−4
{SAMPLE_DIFF.map((line, i) => (
{line.kind === 'hunk' ? (
{line.text}
) : (
<>
{line.n}
{line.mark}
{line.text}
>
)}
))}
);
}
function Steps() {
return (
{SAMPLE_STEPS.map((step, i) => (
{step.state === 'done' ? :
step.state === 'failed' ? :
step.state === 'running' ? :
i + 1}
{step.title}
{step.meta.map((m, j) => {m})}
))}
);
}
// audit entries -> log lines
function AuditLog({ audit }) {
if (!audit || audit.length === 0) return no audit entries yet
;
const levelOf = (a) => a.action === 'reject' || a.to_status === 'failed' ? 'err'
: a.action === 'approve' || a.to_status === 'done' ? 'ok'
: a.to_status === 'review' ? 'warn' : 'info';
return (
{audit.map(a => (
{new Date(a.created_at).toLocaleTimeString()}
{a.action}
{a.actor}
{a.from_status || a.to_status ? ` · ${a.from_status || '∅'} → ${a.to_status || '∅'}` : ''}
))}
);
}
function ResultDiff({ result }) {
const diff = result?.diff;
if (!diff) return no diff — agent has not produced a change yet
;
if (typeof diff === 'string') {
return ;
}
return {JSON.stringify(diff, null, 2)} ;
}
function LiveLogs({ logs }) {
const endRef = React.useRef(null);
React.useEffect(() => { endRef.current?.scrollIntoView({ block: 'end' }); }, [logs]);
if (!logs || logs.length === 0) return no log output yet
;
return (
{logs.map(l => (
{new Date(l.created_at).toLocaleTimeString()}
{l.level}
{l.msg}
))}
);
}
function DetailPanel({ task, onClose, actions, logs }) {
const [tab, setTab] = React.useState('live');
const [detail, setDetail] = React.useState(null);
const open = !!task;
const act = actions || {};
// fetch real detail (audit + allowedNext) when task changes
React.useEffect(() => {
if (!task) { setDetail(null); return; }
let live = true;
window.ORC_API.getTask(task.id).then(d => { if (live) setDetail(d); }).catch(() => {});
return () => { live = false; };
}, [task && task.id, task && task.column]);
if (!task) {
return ;
}
const { PROJECTS } = window.ORC_DATA;
const project = (PROJECTS || []).find(p => p.id === task.project) || { color: 'var(--c-text-3)', name: task.project };
const typeLabel = {
'module-install': 'Module Install',
'update': 'Update',
'agent-task': 'Agent Task',
'system-task': 'System Task',
}[task.type] || task.type;
const audit = detail?.audit || [];
const allowedNext = detail?.allowedNext || [];
const result = detail?.task?.result;
return (
);
}
window.Board = Board;
window.DetailPanel = DetailPanel;