mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
refactor(server): rename dashboard row id → ws_id for v1 row-shape consistency
The /v1/api/dashboard endpoint was the last workstream-listing surface
keyed on `id` rather than `ws_id`. The Stage 2 list-verb lift converged
the active list (`/v1/api/workstreams`) and saved list
(`/v1/api/workstreams/saved`) on `ws_id` but explicitly left dashboard
alone to keep that PR's diff focused. This lands the same rename on
the remaining endpoint so v1 row shape is consistent across the family.
Scope kept narrow:
- Pydantic `DashboardWorkstream` and TS SDK `DashboardWorkstream`
interface both rename `id: str/string` → `ws_id`.
- The bundled web UI (`turnstone/ui/static/app.js`) is the only consumer
reading `dashboard.workstreams[].id` and is updated atomically.
- Console `_fetch_live_block` (cluster-inspect's projection over a
remote node's dashboard payload at `turnstone/console/server.py`)
flips its `entry.get("id")` lookup to `entry.get("ws_id")`.
- Drive-by: stale `id` example in `docs/api-reference.md` for the
earlier `/v1/api/workstreams` rename also fixed.
`_build_node_snapshot` (the global-events SSE node_snapshot payload
consumed by the cluster collector) deliberately stays on `id` — it's
part of a separate cluster-row family (collector → cluster_workstreams
→ console UI) that is internally consistent on `id` and would need its
own coordinated sweep. CHANGELOG documents the bounded blast radius.
Tests: 4554 passing (-m "not live"). ruff + mypy clean.
This commit is contained in:
committed by
Patrick Buckley
parent
3abd2c441b
commit
6572437c5d
@@ -17,6 +17,23 @@ Three release tracks are maintained:
|
||||
|
||||
### Changed
|
||||
|
||||
- **Dashboard row shape: ``id`` → ``ws_id``.** The
|
||||
``GET /v1/api/dashboard`` row dict now keys the workstream
|
||||
identifier as ``ws_id`` (matching the rest of the v1 workstream
|
||||
surface — active list, saved list, history, detail). The Stage 2
|
||||
list-verb lift converged ``/v1/api/workstreams`` and
|
||||
``/v1/api/workstreams/saved`` on ``ws_id`` but left dashboard
|
||||
alone to keep that PR's diff focused; this lands the same rename
|
||||
on the remaining endpoint so the v1 row shape is consistent
|
||||
across the family. Pydantic ``DashboardWorkstream`` and the
|
||||
TypeScript SDK ``DashboardWorkstream`` interface both rename the
|
||||
field accordingly. The bundled web UI is the only consumer that
|
||||
reads ``dashboard.workstreams[].id`` and is updated atomically;
|
||||
no external SDK on a stable line reads the field, so the swap is
|
||||
bounded by normal static-asset reload. Console
|
||||
``_fetch_live_block`` (cluster-inspect's projection over a
|
||||
remote node's dashboard payload) is updated to match.
|
||||
|
||||
- **Coordinator gains rich `ws_state` payload + live activity broadcast**
|
||||
([§ Post-P3 reckoning item #2 follow-up]). Pre-lift coord's
|
||||
cluster broadcast was state-only — the dashboard's coord rows
|
||||
|
||||
@@ -571,8 +571,8 @@ Returns a list of all active workstreams.
|
||||
```json
|
||||
{
|
||||
"workstreams": [
|
||||
{"id": "abc123", "name": "default", "state": "idle"},
|
||||
{"id": "def456", "name": "hacker-news", "state": "thinking"}
|
||||
{"ws_id": "abc123", "name": "default", "state": "idle"},
|
||||
{"ws_id": "def456", "name": "hacker-news", "state": "thinking"}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -581,7 +581,7 @@ Each workstream object:
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------------|-------------|--------------------------------------------------------|
|
||||
| `id` | string | Unique workstream routing identifier |
|
||||
| `ws_id` | string | Unique workstream routing identifier |
|
||||
| `name` | string | Display name (alias if set, otherwise `ws-xxxx`) |
|
||||
| `state` | string | Current state (see state values above) |
|
||||
|
||||
|
||||
@@ -2342,9 +2342,10 @@
|
||||
"type": "object"
|
||||
},
|
||||
"DashboardWorkstream": {
|
||||
"description": "Dashboard row shape for ``GET /v1/api/dashboard``.\n\nRenamed ``id`` \u2192 ``ws_id`` for v1 row-shape consistency with\nthe rest of the workstream surface (active list, saved list,\nhistory, detail, etc.). Frontend consumers reading\n``dashboard.workstreams[].id`` swap to ``.ws_id``.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"title": "Id",
|
||||
"ws_id": {
|
||||
"title": "Ws Id",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
@@ -2423,7 +2424,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"ws_id",
|
||||
"name",
|
||||
"state"
|
||||
],
|
||||
|
||||
@@ -201,7 +201,7 @@ export interface WorkstreamHistoryResponse {
|
||||
}
|
||||
|
||||
export interface DashboardWorkstream {
|
||||
id: string;
|
||||
ws_id: string;
|
||||
name: string;
|
||||
state: string;
|
||||
title?: string;
|
||||
|
||||
@@ -1565,7 +1565,7 @@ def test_cluster_inspect_node_backed_success(storage):
|
||||
payload = {
|
||||
"workstreams": [
|
||||
{
|
||||
"id": ws_id,
|
||||
"ws_id": ws_id,
|
||||
"state": "running",
|
||||
"tokens": 512,
|
||||
"context_ratio": 0.25,
|
||||
@@ -1608,7 +1608,7 @@ def test_cluster_inspect_node_backed_pending_approval_synthesized(storage):
|
||||
payload = {
|
||||
"workstreams": [
|
||||
{
|
||||
"id": ws_id,
|
||||
"ws_id": ws_id,
|
||||
"state": "attention",
|
||||
"activity_state": "approval",
|
||||
"activity": "awaiting approval",
|
||||
@@ -1661,7 +1661,7 @@ def test_cluster_inspect_node_missing_entry_live_null(storage):
|
||||
_seed_node_workstream(storage, ws_id=ws_id, node_id="node-a")
|
||||
payload = {
|
||||
"workstreams": [
|
||||
{"id": "different-" + "x" * 24, "state": "idle"},
|
||||
{"ws_id": "different-" + "x" * 24, "state": "idle"},
|
||||
]
|
||||
}
|
||||
client = _make_client(storage, coord_mgr=mgr, registry=_fake_registry())
|
||||
|
||||
@@ -60,7 +60,7 @@ async def test_dashboard():
|
||||
{
|
||||
"workstreams": [
|
||||
{
|
||||
"id": "ws1",
|
||||
"ws_id": "ws1",
|
||||
"name": "demo",
|
||||
"state": "idle",
|
||||
"tokens": 100,
|
||||
|
||||
@@ -318,13 +318,13 @@ class TestDashboardCache4xxLogLevel:
|
||||
calls["n"] += 1
|
||||
if calls["n"] == 1:
|
||||
return httpx.Response(403, text="forbidden")
|
||||
return httpx.Response(200, json={"workstreams": [{"id": "ws-1"}]})
|
||||
return httpx.Response(200, json={"workstreams": [{"ws_id": "ws-1"}]})
|
||||
|
||||
client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
|
||||
first = await cache.get("node-1", "http://node-1:8001", client, {})
|
||||
second = await cache.get("node-1", "http://node-1:8001", client, {})
|
||||
assert first is None
|
||||
assert second == {"workstreams": [{"id": "ws-1"}]}
|
||||
assert second == {"workstreams": [{"ws_id": "ws-1"}]}
|
||||
assert calls["n"] == 2, "4xx must bypass the cache so the retry reaches upstream"
|
||||
|
||||
|
||||
|
||||
@@ -220,7 +220,15 @@ class ListWorkstreamsResponse(BaseModel):
|
||||
|
||||
|
||||
class DashboardWorkstream(BaseModel):
|
||||
id: str
|
||||
"""Dashboard row shape for ``GET /v1/api/dashboard``.
|
||||
|
||||
Renamed ``id`` → ``ws_id`` for v1 row-shape consistency with
|
||||
the rest of the workstream surface (active list, saved list,
|
||||
history, detail, etc.). Frontend consumers reading
|
||||
``dashboard.workstreams[].id`` swap to ``.ws_id``.
|
||||
"""
|
||||
|
||||
ws_id: str
|
||||
name: str
|
||||
state: str
|
||||
title: str = ""
|
||||
|
||||
@@ -809,7 +809,7 @@ async def _fetch_live_block(
|
||||
if payload is None:
|
||||
return None
|
||||
for entry in payload.get("workstreams", []) or []:
|
||||
if isinstance(entry, dict) and entry.get("id") == ws_id:
|
||||
if isinstance(entry, dict) and entry.get("ws_id") == ws_id:
|
||||
live = {k: entry.get(k) for k in _CLUSTER_WS_LIVE_KEYS if k in entry}
|
||||
# Derived field — kept in lockstep with
|
||||
# _coordinator_live_snapshot so both origins produce the
|
||||
|
||||
+2
-9
@@ -1135,14 +1135,7 @@ async def global_events_sse(request: Request) -> Response:
|
||||
|
||||
|
||||
async def dashboard(request: Request) -> JSONResponse:
|
||||
"""GET /v1/api/dashboard — enriched workstream data + aggregate stats.
|
||||
|
||||
NOTE: row shape still uses ``"id"`` (not ``"ws_id"``) — Stage 2's
|
||||
list-verb lift converged ``/v1/api/workstreams`` and
|
||||
``/saved`` on ``ws_id`` but left dashboard alone to keep that
|
||||
PR's diff focused. The same rename should land here as a separate
|
||||
cleanup so the v1 row shape is consistent across the family.
|
||||
"""
|
||||
"""GET /v1/api/dashboard — enriched workstream data + aggregate stats."""
|
||||
from turnstone.core.memory import get_workstream_display_name
|
||||
|
||||
mgr: SessionManager = request.app.state.workstreams
|
||||
@@ -1170,7 +1163,7 @@ async def dashboard(request: Request) -> JSONResponse:
|
||||
title = get_workstream_display_name(ws.session.ws_id) or ""
|
||||
ws_list.append(
|
||||
{
|
||||
"id": ws.id,
|
||||
"ws_id": ws.id,
|
||||
"name": title or ws.name,
|
||||
"state": ws.state.value,
|
||||
"title": title,
|
||||
|
||||
@@ -3644,7 +3644,7 @@ function loadDashboard() {
|
||||
renderDashboardTable(wsList, agg);
|
||||
var activeWsIds = {};
|
||||
wsList.forEach(function (ws) {
|
||||
activeWsIds[ws.id] = true;
|
||||
activeWsIds[ws.ws_id] = true;
|
||||
});
|
||||
var savedList = (res[1].workstreams || []).filter(function (s) {
|
||||
return !activeWsIds[s.ws_id];
|
||||
@@ -3674,14 +3674,18 @@ function renderDashboardTable(wsList, agg) {
|
||||
}
|
||||
wsList.forEach(function (ws) {
|
||||
var liveState =
|
||||
(workstreams[ws.id] && workstreams[ws.id].state) || ws.state || "idle";
|
||||
(workstreams[ws.ws_id] && workstreams[ws.ws_id].state) ||
|
||||
ws.state ||
|
||||
"idle";
|
||||
var liveName =
|
||||
(workstreams[ws.id] && workstreams[ws.id].name) || ws.name || ws.id;
|
||||
(workstreams[ws.ws_id] && workstreams[ws.ws_id].name) ||
|
||||
ws.name ||
|
||||
ws.ws_id;
|
||||
var sd = STATE_DISPLAY[liveState] || STATE_DISPLAY.idle;
|
||||
|
||||
var row = document.createElement("div");
|
||||
row.className = "dash-row";
|
||||
row.dataset.wsId = ws.id;
|
||||
row.dataset.wsId = ws.ws_id;
|
||||
row.dataset.state = liveState;
|
||||
row.setAttribute("role", "button");
|
||||
row.setAttribute("tabindex", "0");
|
||||
@@ -3754,12 +3758,12 @@ function renderDashboardTable(wsList, agg) {
|
||||
row.appendChild(sub);
|
||||
|
||||
row.onclick = function () {
|
||||
dashboardSwitchWorkstream(ws.id);
|
||||
dashboardSwitchWorkstream(ws.ws_id);
|
||||
};
|
||||
row.onkeydown = function (e) {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
dashboardSwitchWorkstream(ws.id);
|
||||
dashboardSwitchWorkstream(ws.ws_id);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user