From 430923c8577258445e124f70deb1c662ff2311d8 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 23 May 2026 18:00:57 +0200 Subject: [PATCH] Route dashboard status changes through reconciliation --- dashboard/src/components/status-control.js | 67 +++++++++++++------ ...-WP-0048-ui-state-change-reconciliation.md | 12 +++- 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/dashboard/src/components/status-control.js b/dashboard/src/components/status-control.js index c73deae..a6900a9 100644 --- a/dashboard/src/components/status-control.js +++ b/dashboard/src/components/status-control.js @@ -44,6 +44,9 @@ function ensureStyles() { .status-control-message.status-control-ok { color: #16a34a; } +.status-control-message.status-control-review { + color: #b45309; +} `; document.head.append(style); } @@ -52,12 +55,6 @@ function labelForStatus(status) { return String(status ?? "").replace(/_/g, " "); } -function endpointFor(type, id) { - if (type === "task") return `/tasks/${id}`; - if (type === "workstream") return `/workstreams/${id}`; - throw new Error(`Unsupported status-control type: ${type}`); -} - async function readError(response) { try { const body = await response.json(); @@ -68,6 +65,31 @@ async function readError(response) { } } +async function reconcileStatusChange({entity, type, nextStatus, blockingReason = null}) { + const response = await apiFetch("/reconciliation/state-change", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + target_type: type, + target_id: entity.id, + target_status: nextStatus, + actor: "dashboard", + intent: `${type} status change via dashboard`, + blocking_reason: blockingReason, + apply: true, + }), + }); + if (!response.ok) throw new Error(await readError(response)); + return response.json(); +} + +function messageForReconciliation(result) { + if (result.write_through_result === "applied") return {text: "synced", kind: "ok"}; + if (result.reconciliation_class === "human_confirmation") return {text: "needs review", kind: "review"}; + if (result.reconciliation_class === "deferred") return {text: "queued", kind: ""}; + return {text: "unchanged", kind: ""}; +} + export function statusControl({ entity, type, @@ -100,13 +122,14 @@ export function statusControl({ message.textContent = text; message.classList.toggle("status-control-error", kind === "error"); message.classList.toggle("status-control-ok", kind === "ok"); + message.classList.toggle("status-control-review", kind === "review"); } select.addEventListener("change", async () => { const nextStatus = select.value; if (nextStatus === currentStatus) return; - const payload = {status: nextStatus}; + let blockingReason = null; if (type === "task" && nextStatus === "blocked") { const existingReason = entity?.blocking_reason ?? ""; const reason = existingReason || window.prompt("Blocking reason required for blocked tasks:"); @@ -115,12 +138,12 @@ export function statusControl({ setMessage("unchanged"); return; } - payload.blocking_reason = reason; + blockingReason = reason; } if (type === "task" && nextStatus === "done" && currentStatus !== "done") { const ok = window.confirm( - "Mark this task done? Without token fields, State Hub records a heuristic token event." + "Mark this task done? State Hub will reconcile the workplan file before updating cached state." ); if (!ok) { select.value = currentStatus; @@ -132,20 +155,20 @@ export function statusControl({ select.disabled = true; setMessage("saving"); try { - const response = await apiFetch(endpointFor(type, entity.id), { - method: "PATCH", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify(payload), - }); - if (!response.ok) throw new Error(await readError(response)); - const updated = await response.json(); - Object.assign(entity, updated); - currentStatus = updated.status; - select.value = currentStatus; - setMessage("saved", "ok"); - if (typeof onSaved === "function") onSaved(updated); + const result = await reconcileStatusChange({entity, type, nextStatus, blockingReason}); + const messageResult = messageForReconciliation(result); + if (result.write_through_result === "applied") { + Object.assign(entity, {status: result.target_status}); + if (blockingReason !== null) entity.blocking_reason = blockingReason; + currentStatus = result.target_status; + select.value = currentStatus; + if (typeof onSaved === "function") onSaved(entity, result); + } else { + select.value = currentStatus; + } + setMessage(messageResult.text, messageResult.kind); setTimeout(() => { - if (message.textContent === "saved") setMessage(""); + if (message.textContent === messageResult.text) setMessage(""); }, 2200); } catch (error) { select.value = currentStatus; diff --git a/workplans/STATE-WP-0048-ui-state-change-reconciliation.md b/workplans/STATE-WP-0048-ui-state-change-reconciliation.md index 745fbca..a6d08ec 100644 --- a/workplans/STATE-WP-0048-ui-state-change-reconciliation.md +++ b/workplans/STATE-WP-0048-ui-state-change-reconciliation.md @@ -138,7 +138,7 @@ when known. ```task id: STATE-WP-0048-T05 -status: todo +status: done priority: medium state_hub_task_id: "04025e9f-b1cc-4b73-b95a-2a53bad6b360" ``` @@ -149,6 +149,12 @@ on human review, or out of sync. Done when a dashboard user can tell whether their state change has reached the repo file or still needs reconciliation. +Result 2026-05-23: dashboard status controls now submit state changes through +`POST /reconciliation/state-change` with `apply: true` instead of direct DB +patches. The control reports `synced` when the repo file and cached DB state +were updated, `queued` for deferred reconciliation, and `needs review` for +human-confirmation cases. + ## T06 - Add Conflict Handling ```task @@ -181,6 +187,10 @@ Done when UI state changes are covered as first-class ADR-001 workflows. Progress 2026-05-23: added API tests for classify-only responses, safe write-through, missing-file deferral, and human-confirmation message creation. +Progress 2026-05-23: routed dashboard status controls through the +reconciliation API so UI-originated changes exercise the same write-through and +deferred-record path as API clients. + ## Acceptance Criteria - Dashboard state changes never create silent DB/file divergence.