Files
the-custodian/state-hub/dashboard/src/workstreams/[id].md

4.1 KiB

title
title
Workstream
import {API} from "../components/config.js";
import {fieldRow} from "../components/field-help.js";
const wsId = observable.params.id;
const [raw, taskRows, workplanIndex] = await Promise.all([
  fetch(`${API}/workstreams/${wsId}`)
    .then(r => r.ok ? r.json() : r.json().then(e => ({error: e.detail ?? `HTTP ${r.status}`})))
    .catch(e => ({error: String(e)})),
  fetch(`${API}/tasks/?workstream_id=${wsId}&limit=1000`)
    .then(r => r.ok ? r.json() : [])
    .catch(() => []),
  fetch(`${API}/workstreams/workplan-index`)
    .then(r => r.ok ? r.json() : {workstreams: {}})
    .catch(() => ({workstreams: {}})),
]);
if (raw.error) {
  display(html`<div style="color:red;padding:1rem">⚠️ ${raw.error}</div>`);
} else {
  const workplan = (workplanIndex.workstreams ?? {})[wsId] ?? {};
  const name = raw.title || raw.slug || wsId;
  const shortName = name.length > 60 ? name.slice(0, 60) + "…" : name;
  display(html`<h1 style="font-size:1.1rem;margin-bottom:0.25rem">Workstream · <em>${shortName}</em></h1>`);
  display(html`<p style="margin-top:0"><a href="/">← Overview</a> &nbsp;|&nbsp; <a href="/workstreams">← Workstreams</a> &nbsp;|&nbsp; <a href="/token-cost">← Token Cost</a></p>`);

  display(html`<div class="ws-summary">
    <div><span>Status</span><strong>${raw.status ?? "—"}</strong></div>
    <div><span>Workplan</span><strong>${workplan.filename ?? "not file-backed"}</strong></div>
    <div><span>Tasks</span><strong>${taskRows.length}</strong></div>
  </div>`);

  const statusOrder = {blocked: 0, in_progress: 1, todo: 2, done: 3, cancelled: 4};
  const sortedTasks = [...taskRows].sort((a, b) => {
    const statusCompare = (statusOrder[a.status] ?? 9) - (statusOrder[b.status] ?? 9);
    if (statusCompare !== 0) return statusCompare;
    return (a.title ?? "").localeCompare(b.title ?? "");
  });

  display(html`<h2>Tasks</h2>`);
  if (sortedTasks.length === 0) {
    display(html`<p style="color:gray">No tasks are attached to this workstream.</p>`);
  } else {
    display(html`<table class="task-table">
      <thead><tr><th>Status</th><th>Priority</th><th>Task</th><th>Human</th></tr></thead>
      <tbody>${sortedTasks.map(t => html`<tr>
        <td><span class="task-status task-status-${t.status}">${t.status}</span></td>
        <td>${t.priority ?? "—"}</td>
        <td><a href="/tasks/${t.id}">${t.title ?? t.id}</a></td>
        <td>${t.needs_human ? "yes" : ""}</td>
      </tr>`)}</tbody>
    </table>`);
  }

  const FIELD_ORDER = [
    "id","slug","title","status","topic_id","repo_id","repo_goal_id",
    "created_at","updated_at",
  ];

  const rows = FIELD_ORDER.map(k => fieldRow(k, raw[k] ?? null));
  for (const k of Object.keys(raw)) {
    if (!FIELD_ORDER.includes(k)) rows.push(fieldRow(k, raw[k]));
  }

  display(html`<table style="border-collapse:collapse;width:100%;max-width:640px">
    <colgroup><col style="width:160px"><col></colgroup>
    <tbody>${rows}</tbody>
  </table>`);
}
<style> .ws-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.75rem; margin: 1rem 0 1.25rem; max-width: 760px; } .ws-summary div { background: var(--theme-background-alt); border-radius: 6px; padding: 0.75rem 0.9rem; } .ws-summary span { display: block; color: gray; font-size: 0.72rem; text-transform: uppercase; margin-bottom: 0.2rem; } .ws-summary strong { overflow-wrap: anywhere; } .task-table { border-collapse: collapse; width: 100%; max-width: 900px; margin-bottom: 1.25rem; } .task-table th, .task-table td { border-bottom: 1px solid var(--theme-foreground-faint); padding: 0.42rem 0.5rem; text-align: left; vertical-align: top; } .task-status { border-radius: 4px; display: inline-block; font-size: 0.72rem; padding: 0.12rem 0.38rem; white-space: nowrap; } .task-status-done { background: #e8f5e9; color: #1b5e20; } .task-status-in_progress { background: #e3f2fd; color: #0d47a1; } .task-status-blocked { background: #fff3e0; color: #bf360c; } .task-status-todo { background: #f1f5f9; color: #334155; } .task-status-cancelled { background: #f3f4f6; color: #6b7280; } </style>