generated from coulomb/repo-seed
feat: add workplan aliases and legacy meter
Adds preferred workplan REST/event surfaces, legacy-meter telemetry and weekly review summaries, documentation/dashboard terminology updates, dashboard API loading fixes, and close-out sync for STATE-WP-0052 and STATE-WP-0054.
This commit is contained in:
@@ -1,4 +1,65 @@
|
||||
export const API = "http://127.0.0.1:8000";
|
||||
export const DEFAULT_API = "http://127.0.0.1:8000";
|
||||
export const API_STORAGE_KEY = "stateHubApiBase";
|
||||
const API_QUERY_PARAMS = ["api_base", "apiBase"];
|
||||
|
||||
function cleanApiBase(value) {
|
||||
if (typeof value !== "string") return null;
|
||||
const cleaned = value.trim().replace(/\/+$/, "");
|
||||
return cleaned || null;
|
||||
}
|
||||
|
||||
function getStorageApiBase(storage) {
|
||||
if (!storage?.getItem) return null;
|
||||
try {
|
||||
return cleanApiBase(storage.getItem(API_STORAGE_KEY));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function urlFromLocation(location) {
|
||||
if (!location) return null;
|
||||
try {
|
||||
return new URL(location.href ?? String(location));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getQueryApiBase(url) {
|
||||
if (!url) return null;
|
||||
for (const name of API_QUERY_PARAMS) {
|
||||
const value = cleanApiBase(url.searchParams.get(name));
|
||||
if (value) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function inferApiBase(url) {
|
||||
if (!url || !["http:", "https:"].includes(url.protocol)) return DEFAULT_API;
|
||||
if (url.hostname === "::1" || url.hostname === "[::1]") return DEFAULT_API;
|
||||
const apiUrl = new URL(url.href);
|
||||
apiUrl.port = globalThis.STATE_HUB_API_PORT || "8000";
|
||||
apiUrl.pathname = "";
|
||||
apiUrl.search = "";
|
||||
apiUrl.hash = "";
|
||||
return apiUrl.origin;
|
||||
}
|
||||
|
||||
export function resolveApiBase({
|
||||
location = globalThis.location,
|
||||
storage = globalThis.localStorage,
|
||||
} = {}) {
|
||||
const url = urlFromLocation(location);
|
||||
return (
|
||||
getQueryApiBase(url)
|
||||
|| cleanApiBase(globalThis.STATE_HUB_API_BASE)
|
||||
|| getStorageApiBase(storage)
|
||||
|| inferApiBase(url)
|
||||
);
|
||||
}
|
||||
|
||||
export const API = resolveApiBase();
|
||||
export const POLL = 15_000;
|
||||
export const POLL_HEAVY = 60_000;
|
||||
export const FETCH_TIMEOUT = 12_000;
|
||||
|
||||
@@ -26,7 +26,7 @@ const FIELD_LINKS = {
|
||||
getTitle: d => d.title,
|
||||
},
|
||||
workstream_id: {
|
||||
apiUrl: id => `${API}/workstreams/${id}`,
|
||||
apiUrl: id => `${API}/workplans/${id}`,
|
||||
getUrl: (id, _d) => `/workstreams/${id}`,
|
||||
getTitle: d => d.title || d.slug,
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ const depState = (async function*() {
|
||||
let wsMap = {}, edges = [], ok = false;
|
||||
try {
|
||||
const [rw, rto, rr, rd] = await Promise.all([
|
||||
apiFetch("/workstreams/"),
|
||||
apiFetch("/workplans/"),
|
||||
apiFetch("/topics/"),
|
||||
apiFetch("/repos/"),
|
||||
apiFetch("/state/deps"),
|
||||
|
||||
@@ -63,7 +63,7 @@ Current loaders:
|
||||
| File | API endpoint |
|
||||
|---|---|
|
||||
| `summary.json.py` | `/state/summary` |
|
||||
| `workstreams.json.py` | `/workstreams/` |
|
||||
| `workstreams.json.py` | `/workplans/` |
|
||||
| `contributions.json.py` | `/contributions/` |
|
||||
| `decisions.json.py` | `/decisions/` |
|
||||
| `domains.json.py` | `/domains/` |
|
||||
@@ -144,7 +144,7 @@ The dashboard has 30+ pages organised in four navigation groups:
|
||||
|
||||
| Page | Route | Purpose |
|
||||
|---|---|---|
|
||||
| Workstreams | `/workstreams` | All workstreams with Workstream Health Index |
|
||||
| Workplans | `/workstreams` | All workplans with Workplan Health Index; route name remains compatibility-backed |
|
||||
| Decisions | `/decisions` | Decision log with resolve-in-place form |
|
||||
| Dependencies | `/dependencies` | Dependency graph explorer |
|
||||
| Extensions | `/extensions` | Extension point registry |
|
||||
@@ -163,8 +163,11 @@ The dashboard has 30+ pages organised in four navigation groups:
|
||||
All shared components live in `src/components/` and are imported as ES modules:
|
||||
|
||||
### `config.js`
|
||||
Exports two constants used by every live-polling page:
|
||||
- `API = "http://127.0.0.1:8000"` — the FastAPI base URL
|
||||
Exports shared runtime configuration used by every live-polling page:
|
||||
- `API` — the FastAPI base URL. It defaults to `http://127.0.0.1:8000`
|
||||
outside the browser, derives from the dashboard host in browser sessions, and
|
||||
can be overridden with `?api_base=...`, `globalThis.STATE_HUB_API_BASE`, or
|
||||
`localStorage.stateHubApiBase`.
|
||||
- `POLL = 15_000` — polling interval in milliseconds
|
||||
|
||||
### `entity-modal.js`
|
||||
|
||||
@@ -62,7 +62,7 @@ create_dependency(
|
||||
Via REST:
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8000/workstreams/<from_id>/dependencies/ \
|
||||
curl -X POST http://127.0.0.1:8000/workplans/<from_id>/dependencies/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"to_workstream_id": "<to_id>", "description": "..."}'
|
||||
```
|
||||
|
||||
@@ -127,7 +127,7 @@ The Goals page groups everything by domain:
|
||||
|
||||
Workstreams carry an optional `repo_goal_id` field. Setting it traces *why* a workstream exists — which specific repo goal it contributes to. This connection is currently recorded in the DB but is not yet visualised in the Workstreams page.
|
||||
|
||||
To set the link when creating a workstream via `create_workstream`, pass `repo_goal_id`. To update an existing one, use `PATCH /workstreams/{id}/` with `{"repo_goal_id": "<uuid>"}`.
|
||||
To set the link when creating a workplan through the preferred API, pass `repo_goal_id`. To update an existing one, use `PATCH /workplans/{id}/` with `{"repo_goal_id": "<uuid>"}`. Legacy `create_workstream` and `/workstreams/{id}/` callers remain compatibility-supported while they are metered.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ make api # db + migrate + uvicorn (restarts if already running)
|
||||
| Page | Endpoints |
|
||||
|---|---|
|
||||
| Overview | `/state/summary` |
|
||||
| Workstreams | `/workstreams/`, `/topics/`, `/state/summary` |
|
||||
| Workplans | `/workplans/`, `/topics/`, `/state/summary` |
|
||||
| Decisions | `/decisions/?limit=500`, `/topics/` |
|
||||
| Progress | `/progress/?limit=500` |
|
||||
|
||||
|
||||
@@ -83,8 +83,8 @@ and summary.
|
||||
## Data source
|
||||
|
||||
Polls `GET /state/summary` every **15 seconds**. The workstream chart also polls
|
||||
`GET /workstreams/`, `GET /tasks/?limit=2000`, `GET /topics/`, `GET /repos/`,
|
||||
and `GET /workstreams/workplan-index` for repository grouping, task counts, and
|
||||
`GET /workplans/`, `GET /tasks/?limit=2000`, `GET /topics/`, `GET /repos/`,
|
||||
and `GET /workplans/index` for repository grouping, task counts, and
|
||||
workplan filename tooltips. Blocking decisions are fetched separately via
|
||||
`GET /decisions/?decision_type=pending` and only re-fetched after a successful
|
||||
resolve action — this prevents the inline form from being wiped on every poll.
|
||||
|
||||
@@ -95,5 +95,5 @@ status group.
|
||||
|
||||
Polls every **15 seconds**:
|
||||
- `GET /tasks/?limit=500`
|
||||
- `GET /workstreams/`
|
||||
- `GET /workplans/`
|
||||
- `GET /topics/`
|
||||
|
||||
@@ -64,7 +64,7 @@ to this reference page.
|
||||
|
||||
Polls every **15 seconds**:
|
||||
- `GET /tasks/?limit=500` — all tasks
|
||||
- `GET /workstreams/` — for domain + title context
|
||||
- `GET /workplans/` — for domain + title context
|
||||
- `GET /topics/` — for domain slug resolution
|
||||
- `GET /contributions/` — for third-party todos
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ advance_workstation(entity_type="workstream", entity_id="<uuid>", target_worksta
|
||||
Direct status patching still exists for bootstrap and compatibility work:
|
||||
|
||||
```bash
|
||||
curl -X PATCH http://127.0.0.1:8000/workstreams/<uuid>/ \
|
||||
curl -X PATCH http://127.0.0.1:8000/workplans/<uuid>/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"status": "finished"}'
|
||||
```
|
||||
|
||||
@@ -108,7 +108,7 @@ create_workstream(
|
||||
Via REST:
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8000/workstreams/ \
|
||||
curl -X POST http://127.0.0.1:8000/workplans/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"topic_id": "<uuid>", "title": "…", "status": "ready"}'
|
||||
```
|
||||
|
||||
@@ -16,7 +16,7 @@ GET /progress/?event_type=daily_triage&limit=14
|
||||
|
||||
Each event carries the report under `detail.report`, with a summary and a list
|
||||
of recommendations. Candidate values are resolved through
|
||||
`/workstreams/workplan-index` so file-backed workplans can link to their
|
||||
`/workplans/index` so file-backed workplans can link to their
|
||||
workstream detail pages.
|
||||
|
||||
## How to read recommendations
|
||||
|
||||
@@ -39,11 +39,11 @@ const pageState = (async function*() {
|
||||
loadJson("summary", "/state/summary", {timeout: 20_000}),
|
||||
loadJson("sbom snapshots", "/sbom/snapshots/"),
|
||||
loadJson("milestones", "/progress/?event_type=milestone&limit=500"),
|
||||
loadJson("workstreams", "/workstreams/"),
|
||||
loadJson("workplans", "/workplans/"),
|
||||
loadJson("tasks", "/tasks/?limit=2000"),
|
||||
loadJson("topics", "/topics/"),
|
||||
loadJson("repos", "/repos/"),
|
||||
loadJson("workplan index", "/workstreams/workplan-index").catch(() => ({workstreams: {}})),
|
||||
loadJson("workplan index", "/workplans/index").catch(() => ({workplans: {}, workstreams: {}})),
|
||||
]);
|
||||
|
||||
ok = true;
|
||||
|
||||
@@ -15,7 +15,7 @@ const interventionState = (async function*() {
|
||||
try {
|
||||
const [rt, rw, rto, rr] = await Promise.all([
|
||||
apiFetch("/tasks/?limit=500"),
|
||||
apiFetch("/workstreams/"),
|
||||
apiFetch("/workplans/"),
|
||||
apiFetch("/topics/"),
|
||||
apiFetch("/repos/"),
|
||||
]);
|
||||
|
||||
@@ -16,7 +16,7 @@ try {
|
||||
fetch(`${API}/sbom/`).then(r => r.ok ? r.json() : []),
|
||||
fetch(`${API}/extension-points/`).then(r => r.ok ? r.json() : []),
|
||||
fetch(`${API}/technical-debt/`).then(r => r.ok ? r.json() : []),
|
||||
fetch(`${API}/workstreams/`).then(r => r.ok ? r.json() : []),
|
||||
fetch(`${API}/workplans/`).then(r => r.ok ? r.json() : []),
|
||||
]);
|
||||
} catch {}
|
||||
```
|
||||
|
||||
@@ -14,7 +14,7 @@ const taskState = (async function*() {
|
||||
try {
|
||||
const [rt, rw, rto, rr] = await Promise.all([
|
||||
apiFetch("/tasks/?limit=500"),
|
||||
apiFetch("/workstreams/"),
|
||||
apiFetch("/workplans/"),
|
||||
apiFetch("/topics/"),
|
||||
apiFetch("/repos/"),
|
||||
]);
|
||||
|
||||
@@ -14,7 +14,7 @@ const tdState = (async function*() {
|
||||
try {
|
||||
const [rt, rw, rto, rr] = await Promise.all([
|
||||
apiFetch("/technical-debt/"),
|
||||
apiFetch("/workstreams/"),
|
||||
apiFetch("/workplans/"),
|
||||
apiFetch("/topics/"),
|
||||
apiFetch("/repos/"),
|
||||
]);
|
||||
|
||||
@@ -16,7 +16,7 @@ const todoState = (async function*() {
|
||||
try {
|
||||
const [rt, rw, rto, rr, rc, ri] = await Promise.all([
|
||||
apiFetch("/tasks/?limit=500"),
|
||||
apiFetch("/workstreams/"),
|
||||
apiFetch("/workplans/"),
|
||||
apiFetch("/topics/"),
|
||||
apiFetch("/repos/"),
|
||||
apiFetch("/contributions/"),
|
||||
|
||||
@@ -104,7 +104,7 @@ function queueControls(row) {
|
||||
}
|
||||
|
||||
save.onclick = () => run("saving", async () => {
|
||||
const response = await apiFetch(`/execution/workstreams/${row.workstream_id}/intent`, {
|
||||
const response = await apiFetch(`/execution/workplans/${row.workstream_id}/intent`, {
|
||||
method: "PATCH",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify(payload()),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Workstreams
|
||||
title: Workplans
|
||||
---
|
||||
|
||||
```js
|
||||
@@ -16,7 +16,7 @@ const wsState = (async function*() {
|
||||
let data = [], openWs = [], ok = false;
|
||||
try {
|
||||
const [rw, rt, rr, rd] = await Promise.all([
|
||||
apiFetch("/workstreams/"),
|
||||
apiFetch("/workplans/"),
|
||||
apiFetch("/topics/"),
|
||||
apiFetch("/repos/"),
|
||||
apiFetch("/state/deps"),
|
||||
@@ -134,7 +134,7 @@ const _domainBreakdown = [...new Set(openWs.map(w => _idToDomain[w.id] ?? "unkno
|
||||
}).filter(Boolean);
|
||||
```
|
||||
|
||||
# Workstreams
|
||||
# Workplans
|
||||
|
||||
```js
|
||||
import {injectTocTop} from "./components/toc-sidebar.js";
|
||||
@@ -166,17 +166,17 @@ function _warnLevel(name, val) {
|
||||
function _warnColor(lv) { return lv === 2 ? "#dc2626" : lv === 1 ? "#d97706" : "var(--theme-foreground-muted, #666)"; }
|
||||
|
||||
const _whiMetrics = [
|
||||
{name: "DD", val: _DD, fmt: v => v.toFixed(2), label: "Dependency Density", desc: "Average number of dependencies per open workstream; high values indicate a tightly coupled graph that is hard to parallelise."},
|
||||
{name: "BR", val: _BR, fmt: v => (v*100).toFixed(0)+"%", label: "Blocked Ratio", desc: "Share of open workstreams currently in a blocked state; directly reduces the work that can proceed right now."},
|
||||
{name: "SPR", val: _SPR, fmt: v => (v*100).toFixed(0)+"%", label: "Single-Point Risk", desc: "Share of workstreams depended on by others but with no incoming dependencies themselves; losing one stalls everything downstream."},
|
||||
{name: "PEP", val: _PEP, fmt: v => (v*100).toFixed(0)+"%", label: "Parallel Execution Potential", desc: "Share of open workstreams with zero blocking dependencies that could start or continue immediately."},
|
||||
{name: "DD", val: _DD, fmt: v => v.toFixed(2), label: "Dependency Density", desc: "Average number of dependencies per open workplan; high values indicate a tightly coupled graph that is hard to parallelise."},
|
||||
{name: "BR", val: _BR, fmt: v => (v*100).toFixed(0)+"%", label: "Blocked Ratio", desc: "Share of open workplans currently in a blocked state; directly reduces the work that can proceed right now."},
|
||||
{name: "SPR", val: _SPR, fmt: v => (v*100).toFixed(0)+"%", label: "Single-Point Risk", desc: "Share of workplans depended on by others but with no incoming dependencies themselves; losing one stalls everything downstream."},
|
||||
{name: "PEP", val: _PEP, fmt: v => (v*100).toFixed(0)+"%", label: "Parallel Execution Potential", desc: "Share of open workplans with zero blocking dependencies that could start or continue immediately."},
|
||||
{name: "CDDR", val: _CDDR, fmt: v => (v*100).toFixed(0)+"%", label: "Cross-Domain Dependency Ratio", desc: "Share of dependency edges that cross domain boundaries; high values mean progress in one domain is gated on another team or project."},
|
||||
];
|
||||
|
||||
const _whiBox = html`<div class="kpi-infobox whi-box">
|
||||
<div class="kpi-infobox-title">Workstream Health</div>
|
||||
<div class="kpi-infobox-title">Workplan Health</div>
|
||||
${_openCount === 0
|
||||
? html`<div class="kpi-row"><span class="kpi-muted">No active workstreams</span></div>`
|
||||
? html`<div class="kpi-row"><span class="kpi-muted">No active workplans</span></div>`
|
||||
: html`
|
||||
<div class="whi-score-row">
|
||||
<span class="whi-value" style="color:${_whiColor(_WHI)}">${(_WHI*100).toFixed(0)}<span class="whi-pct">%</span></span>
|
||||
@@ -202,7 +202,7 @@ const _whiBox = html`<div class="kpi-infobox whi-box">
|
||||
description="Domain-scoped WHI (intra-domain edges only). Open: ${d.openCount} · Blocked: ${(d.br*100).toFixed(0)}% · Runnable: ${(d.pep*100).toFixed(0)}%"
|
||||
doc="/docs/workstream-health-index">${d.domain}</help-tip>
|
||||
<span class="whi-domain-score" style="color:${_whiColor(d.whi)}">${(d.whi*100).toFixed(0)}%</span>
|
||||
${d.cpi === 1 ? html`<help-tip style="color:#d97706;font-size:0.7rem" label="Dependency Cycle" description="A circular dependency exists within this domain — workstreams are waiting on each other and cannot all proceed." doc="/docs/workstream-health-index">⚠</help-tip>` : ""}
|
||||
${d.cpi === 1 ? html`<help-tip style="color:#d97706;font-size:0.7rem" label="Dependency Cycle" description="A circular dependency exists within this domain — workplans are waiting on each other and cannot all proceed." doc="/docs/workstream-health-index">⚠</help-tip>` : ""}
|
||||
</div>`)}
|
||||
</div>` : ""}
|
||||
`}
|
||||
@@ -276,7 +276,7 @@ display(Plot.plot({
|
||||
}));
|
||||
```
|
||||
|
||||
## All Workstreams
|
||||
## All Workplans
|
||||
|
||||
```js
|
||||
display(_filtersForm);
|
||||
@@ -313,7 +313,7 @@ const wsWithDeps = openWs.filter(w => {
|
||||
});
|
||||
|
||||
if (wsWithDeps.length === 0) {
|
||||
display(html`<p class="dim">No dependency edges recorded for the current filter. Use <code>create_dependency()</code> via the MCP server to link workstreams.</p>`);
|
||||
display(html`<p class="dim">No dependency edges recorded for the current filter. Use <code>create_dependency()</code> via the MCP server to link workplans.</p>`);
|
||||
} else {
|
||||
display(html`<div class="dep-grid">${wsWithDeps.map(w => {
|
||||
const depRows = w.depends_on.map(d =>
|
||||
|
||||
@@ -11,13 +11,13 @@ import {statusControl, TASK_STATUSES, WORKSTREAM_STATUSES} from "../components/s
|
||||
```js
|
||||
const wsId = observable.params.id;
|
||||
const [raw, taskRows, workplanIndex] = await Promise.all([
|
||||
fetch(`${API}/workstreams/${wsId}`)
|
||||
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}/workstreams/workplan-index`)
|
||||
fetch(`${API}/workplans/index`)
|
||||
.then(r => r.ok ? r.json() : {workstreams: {}})
|
||||
.catch(() => ({workstreams: {}})),
|
||||
]);
|
||||
@@ -31,7 +31,7 @@ if (raw.error) {
|
||||
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> | <a href="/workstreams">← Workstreams</a> | <a href="/token-cost">← Token Cost</a></p>`);
|
||||
display(html`<p style="margin-top:0"><a href="/">← Overview</a> | <a href="/workstreams">← Workplans</a> | <a href="/token-cost">← Token Cost</a></p>`);
|
||||
|
||||
display(html`<div class="ws-summary">
|
||||
<div><span>Status</span>${statusControl({
|
||||
|
||||
@@ -27,7 +27,7 @@ const triageState = (async function*() {
|
||||
try {
|
||||
const [reportsResp, indexResp] = await Promise.all([
|
||||
apiFetch("/progress/?event_type=daily_triage&limit=14"),
|
||||
apiFetch("/workstreams/workplan-index"),
|
||||
apiFetch("/workplans/index"),
|
||||
]);
|
||||
ok = reportsResp.ok && indexResp.ok;
|
||||
events = reportsResp.ok ? await reportsResp.json() : [];
|
||||
|
||||
Reference in New Issue
Block a user