generated from coulomb/repo-seed
Route dashboard status changes through reconciliation
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user