--- title: Tasks --- ```js import {API, POLL_HEAVY, apiFetch, pollDelay, waitForVisible} from "./components/config.js"; ``` ```js const taskState = (async function*() { let failures = 0; while (true) { let data = [], ok = false; try { const [rt, rw, rto, rr] = await Promise.all([ apiFetch("/tasks/?limit=500"), apiFetch("/workplans/"), apiFetch("/topics/"), apiFetch("/repos/"), ]); ok = rt.ok && rw.ok && rto.ok && rr.ok; if (ok) { const [taskList, wsList, topicList, repoList] = await Promise.all([rt.json(), rw.json(), rto.json(), rr.json()]); const topicMap = Object.fromEntries(topicList.map(t => [t.id, t])); const repoMap = Object.fromEntries(repoList.map(r => [r.id, r])); const wsMap = Object.fromEntries(wsList.map(w => [w.id, { ...w, domain: repoMap[w.repo_id]?.domain_slug ?? topicMap[w.topic_id]?.domain_slug ?? "unknown", }])); data = taskList.map(t => ({ ...t, workstream_title: wsMap[t.workstream_id]?.title ?? "—", domain: wsMap[t.workstream_id]?.domain ?? "unknown", })); } } catch {} failures = ok ? 0 : failures + 1; yield {data, ok, ts: new Date()}; await waitForVisible(pollDelay({ok, base: POLL_HEAVY, failures})); } })(); ``` ```js const data = taskState.data ?? []; const _ok = taskState.ok ?? false; const _ts = taskState.ts; ``` ```js import {MultiSelect} from "./components/multiselect.js"; const STATUSES = ["wait", "todo", "progress", "done", "cancel"]; const PRIORITIES = ["critical", "high", "medium", "low"]; const _domainsResp = await fetch(`${API}/domains/?status=active`).catch(() => null); const DOMAINS = _domainsResp?.ok ? (await _domainsResp.json()).map(d => d.slug) : ["custodian", "railiance", "markitect", "coulomb_social", "personhood", "foerster_capabilities"]; const _filtersForm = Inputs.form( { status: MultiSelect(STATUSES, {label: "Status", placeholder: "All statuses"}), priority: MultiSelect(PRIORITIES, {label: "Priority", placeholder: "All priorities"}), domain: MultiSelect(DOMAINS, {label: "Domain", placeholder: "All domains"}), assignee: Inputs.text({placeholder: "Assignee…", style: "width:120px"}), }, { template: ({status, priority, domain, assignee}) => html`
${status}${priority}${domain}
${assignee}
`, } ); ``` ```js const filters = Generators.input(_filtersForm); ``` ```js const filtered = data.filter(t => (filters.status.length === 0 || filters.status.includes(t.status)) && (filters.priority.length === 0 || filters.priority.includes(t.priority)) && (filters.domain.length === 0 || filters.domain.includes(t.domain)) && (!filters.assignee || (t.assignee ?? "").toLowerCase().includes(filters.assignee.toLowerCase())) ); ``` # Tasks ```js import {injectTocTop} from "./components/toc-sidebar.js"; import {withDocHelp} from "./components/doc-overlay.js"; import {openEntityModal, buildEntityTable} from "./components/entity-modal.js"; import {statusControl, TASK_STATUSES} from "./components/status-control.js"; // ── KPI sidebar card ───────────────────────────────────────────────────────── const _open = data.filter(t => ["wait", "todo", "progress"].includes(t.status)); const _waiting = data.filter(t => t.status === "wait"); const _inProg = data.filter(t => t.status === "progress"); const _done = data.filter(t => t.status === "done"); const _total = data.filter(t => t.status !== "cancel").length; const _donePct = _total > 0 ? Math.round(_done.length / _total * 100) : 0; const _kpiBox = html`
Task Overview
open
${_open.length}
waiting
${_waiting.length}
progress
${_inProg.length}
done
${_done.length}
${_donePct}% of total
`; // ── Live indicator ──────────────────────────────────────────────────────────── const _liveEl = html`
${_ok ? `Live · updated ${_ts?.toLocaleTimeString()}` : html`Offline — run: make api`}
`; withDocHelp(_liveEl, "/docs/live-data"); const _h1 = document.querySelector("#observablehq-main h1"); if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/tasks"); } // ── Inject into TOC sidebar ─────────────────────────────────────────────────── injectTocTop("task-kpi-box", _kpiBox); injectTocTop("live-indicator", _liveEl); ``` ## Status Distribution ```js import * as Plot from "npm:@observablehq/plot"; const STATUS_COLOR = { wait: "#f59e0b", todo: "#94a3b8", progress: "#8b5cf6", done: "#22c55e", cancel: "#cbd5e1", }; const byStatus = STATUSES .map(s => ({status: s, count: filtered.filter(t => t.status === s).length})) .filter(d => d.count > 0); display(byStatus.length === 0 ? html`

No tasks match the current filter.

` : Plot.plot({ marks: [ Plot.barX(byStatus, {y: "status", x: "count", fill: d => STATUS_COLOR[d.status] ?? "#94a3b8", tip: true}), Plot.ruleX([0]), ], marginLeft: 90, width: 500, }) ); ``` ## Waiting Tasks ```js const _waitingInFilter = filtered.filter(t => t.status === "wait"); if (_waitingInFilter.length === 0) { display(html`

No waiting tasks in current filter. ✓

`); } else { display(html`
${_waitingInFilter.map(t => html`
openEntityModal(t, "task")}>
${t.priority} ${t.domain} ${t.workstream_title} ${t.due_date ? html`${new Date(t.due_date) < new Date() ? "⚠ overdue" : "due"} ${t.due_date}` : ""} ${t.assignee ? html`@${t.assignee}` : ""}
${t.title}
${t.blocking_reason ? html`
⊘ ${t.blocking_reason}
` : ""}
`)}
`); } ``` ## All Tasks ```js display(_filtersForm); display(html`

${filtered.length} tasks shown.

`); const PRIORITY_ORDER = {critical: 0, high: 1, medium: 2, low: 3}; const STATUS_ORDER = {wait: 0, progress: 1, todo: 2, done: 3, cancel: 4}; const sorted = [...filtered].sort((a, b) => { const sd = (STATUS_ORDER[a.status] ?? 9) - (STATUS_ORDER[b.status] ?? 9); if (sd !== 0) return sd; return (PRIORITY_ORDER[a.priority] ?? 9) - (PRIORITY_ORDER[b.priority] ?? 9); }); display(buildEntityTable( sorted, [ {label: "Status", render: t => statusControl({entity: t, type: "task", statuses: TASK_STATUSES})}, {label: "Priority", key: "priority"}, {label: "Title", key: "title", cls: "et-title-col et-title-cell"}, {label: "Domain", key: "domain"}, {label: "Workstream", key: "workstream_title", cls: "et-ws-col et-ws-cell"}, {label: "Assignee", render: t => t.assignee ?? "—"}, {label: "Due", render: t => t.due_date ?? "—"}, ], t => openEntityModal(t, "task"), )); ```