---
title: Workplan
---

```js
import {API} from "../components/config.js";
import {fieldRow} from "../components/field-help.js";
import {statusControl, TASK_STATUSES, WORKSTREAM_STATUSES} from "../components/status-control.js";
```

```js
const wsId = observable.params.id;
const [raw, taskRows, workplanIndex] = await Promise.all([
  fetch(`${API}/workplans/${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}/workplans/index`)
    .then(r => r.ok ? r.json() : {workplans: {}})
    .catch(() => ({workplans: {}})),
]);
```

```js
if (raw.error) {
  display(html`<div style="color:red;padding:1rem">⚠️ ${raw.error}</div>`);
} else {
  const workplan = (workplanIndex.workplans ?? 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">Workplan · <em>${shortName}</em></h1>`);
  display(html`<p style="margin-top:0"><a href="/">← Overview</a> &nbsp;|&nbsp; <a href="/workstreams">← Workplans</a> &nbsp;|&nbsp; <a href="/token-cost">← Token Cost</a></p>`);

  display(html`<div class="ws-summary">
    <div><span>Status</span>${statusControl({
      entity: raw,
      type: "workplan",
      statuses: WORKSTREAM_STATUSES,
      onSaved: () => setTimeout(() => location.reload(), 450),
    })}</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 = {wait: 0, progress: 1, todo: 2, done: 3, cancel: 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 workplan.</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>${statusControl({
          entity: t,
          type: "task",
          statuses: TASK_STATUSES,
          onSaved: () => setTimeout(() => location.reload(), 450),
        })}</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-progress { background: #ede9fe; color: #5b21b6; }
.task-status-wait { background: #fff3e0; color: #bf360c; }
.task-status-todo { background: #f1f5f9; color: #334155; }
.task-status-cancel { background: #f3f4f6; color: #6b7280; }
</style>
