--- title: Tasks --- ```js import {API, POLL} from "./components/config.js"; ``` ```js const taskState = (async function*() { while (true) { let data = [], ok = false; try { const [rt, rw, rto, rr] = await Promise.all([ fetch(`${API}/tasks/?limit=500`), fetch(`${API}/workstreams/`), fetch(`${API}/topics/`), fetch(`${API}/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 {} yield {data, ok, ts: new Date()}; await new Promise(res => setTimeout(res, POLL)); } })(); ``` ```js const data = taskState.data ?? []; const _ok = taskState.ok ?? false; const _ts = taskState.ts; ``` ```js import {MultiSelect} from "./components/multiselect.js"; const STATUSES = ["todo", "in_progress", "blocked", "done", "cancelled"]; 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"; // ── KPI sidebar card ───────────────────────────────────────────────────────── const _open = data.filter(t => ["todo", "in_progress", "blocked"].includes(t.status)); const _blocked = data.filter(t => t.status === "blocked"); const _inProg = data.filter(t => t.status === "in_progress"); const _done = data.filter(t => t.status === "done"); const _total = data.filter(t => t.status !== "cancelled").length; const _donePct = _total > 0 ? Math.round(_done.length / _total * 100) : 0; const _kpiBox = html`
Task Overview
open
${_open.length}
blocked
${_blocked.length}
in 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 = { todo: "#94a3b8", in_progress: "#3b82f6", blocked: "#ef4444", done: "#22c55e", cancelled: "#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, }) ); ``` ## Blocked Tasks ```js const _blockedInFilter = filtered.filter(t => t.status === "blocked"); if (_blockedInFilter.length === 0) { display(html`

No blocked tasks in current filter. ✓

`); } else { display(html`
${_blockedInFilter.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 = {blocked: 0, in_progress: 1, todo: 2, done: 3, cancelled: 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", key: "status"}, {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"), )); ```