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 {
|
.status-control-message.status-control-ok {
|
||||||
color: #16a34a;
|
color: #16a34a;
|
||||||
}
|
}
|
||||||
|
.status-control-message.status-control-review {
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
document.head.append(style);
|
document.head.append(style);
|
||||||
}
|
}
|
||||||
@@ -52,12 +55,6 @@ function labelForStatus(status) {
|
|||||||
return String(status ?? "").replace(/_/g, " ");
|
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) {
|
async function readError(response) {
|
||||||
try {
|
try {
|
||||||
const body = await response.json();
|
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({
|
export function statusControl({
|
||||||
entity,
|
entity,
|
||||||
type,
|
type,
|
||||||
@@ -100,13 +122,14 @@ export function statusControl({
|
|||||||
message.textContent = text;
|
message.textContent = text;
|
||||||
message.classList.toggle("status-control-error", kind === "error");
|
message.classList.toggle("status-control-error", kind === "error");
|
||||||
message.classList.toggle("status-control-ok", kind === "ok");
|
message.classList.toggle("status-control-ok", kind === "ok");
|
||||||
|
message.classList.toggle("status-control-review", kind === "review");
|
||||||
}
|
}
|
||||||
|
|
||||||
select.addEventListener("change", async () => {
|
select.addEventListener("change", async () => {
|
||||||
const nextStatus = select.value;
|
const nextStatus = select.value;
|
||||||
if (nextStatus === currentStatus) return;
|
if (nextStatus === currentStatus) return;
|
||||||
|
|
||||||
const payload = {status: nextStatus};
|
let blockingReason = null;
|
||||||
if (type === "task" && nextStatus === "blocked") {
|
if (type === "task" && nextStatus === "blocked") {
|
||||||
const existingReason = entity?.blocking_reason ?? "";
|
const existingReason = entity?.blocking_reason ?? "";
|
||||||
const reason = existingReason || window.prompt("Blocking reason required for blocked tasks:");
|
const reason = existingReason || window.prompt("Blocking reason required for blocked tasks:");
|
||||||
@@ -115,12 +138,12 @@ export function statusControl({
|
|||||||
setMessage("unchanged");
|
setMessage("unchanged");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
payload.blocking_reason = reason;
|
blockingReason = reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "task" && nextStatus === "done" && currentStatus !== "done") {
|
if (type === "task" && nextStatus === "done" && currentStatus !== "done") {
|
||||||
const ok = window.confirm(
|
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) {
|
if (!ok) {
|
||||||
select.value = currentStatus;
|
select.value = currentStatus;
|
||||||
@@ -132,20 +155,20 @@ export function statusControl({
|
|||||||
select.disabled = true;
|
select.disabled = true;
|
||||||
setMessage("saving");
|
setMessage("saving");
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(endpointFor(type, entity.id), {
|
const result = await reconcileStatusChange({entity, type, nextStatus, blockingReason});
|
||||||
method: "PATCH",
|
const messageResult = messageForReconciliation(result);
|
||||||
headers: {"Content-Type": "application/json"},
|
if (result.write_through_result === "applied") {
|
||||||
body: JSON.stringify(payload),
|
Object.assign(entity, {status: result.target_status});
|
||||||
});
|
if (blockingReason !== null) entity.blocking_reason = blockingReason;
|
||||||
if (!response.ok) throw new Error(await readError(response));
|
currentStatus = result.target_status;
|
||||||
const updated = await response.json();
|
select.value = currentStatus;
|
||||||
Object.assign(entity, updated);
|
if (typeof onSaved === "function") onSaved(entity, result);
|
||||||
currentStatus = updated.status;
|
} else {
|
||||||
select.value = currentStatus;
|
select.value = currentStatus;
|
||||||
setMessage("saved", "ok");
|
}
|
||||||
if (typeof onSaved === "function") onSaved(updated);
|
setMessage(messageResult.text, messageResult.kind);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (message.textContent === "saved") setMessage("");
|
if (message.textContent === messageResult.text) setMessage("");
|
||||||
}, 2200);
|
}, 2200);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
select.value = currentStatus;
|
select.value = currentStatus;
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ when known.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: STATE-WP-0048-T05
|
id: STATE-WP-0048-T05
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "04025e9f-b1cc-4b73-b95a-2a53bad6b360"
|
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
|
Done when a dashboard user can tell whether their state change has reached the
|
||||||
repo file or still needs reconciliation.
|
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
|
## T06 - Add Conflict Handling
|
||||||
|
|
||||||
```task
|
```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
|
Progress 2026-05-23: added API tests for classify-only responses, safe
|
||||||
write-through, missing-file deferral, and human-confirmation message creation.
|
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
|
## Acceptance Criteria
|
||||||
|
|
||||||
- Dashboard state changes never create silent DB/file divergence.
|
- Dashboard state changes never create silent DB/file divergence.
|
||||||
|
|||||||
Reference in New Issue
Block a user