feat(kanban): expose redacted sync diagnostics

req: operations/001

req: security/002

req: sync/016
This commit is contained in:
slhx agent
2026-07-13 20:06:25 +02:00
parent fbc0c9db30
commit f1dc6edb7a
5 changed files with 188 additions and 11 deletions
+3
View File
@@ -51,6 +51,7 @@ function migrateCommandLog(request, oldVersion) {
schemaVersion: COMMAND_SCHEMA,
targetColumn: command.targetColumn || "done",
accountPartition: command.accountPartition || "demo:demo",
queuedAt: Number.isSafeInteger(command.queuedAt) ? command.queuedAt : Date.now(),
});
}
meta.put({ from: oldVersion, to: DATABASE_VERSION, migrated: legacy.length }, MIGRATION_KEY);
@@ -139,6 +140,7 @@ async function appendReorder(database, accountPartition, wire) {
actor,
session: stableSession(),
causal,
queuedAt: Date.now(),
kind: "reorder_card",
cardId: String(wire[2] || "1"),
targetColumn: "done",
@@ -199,6 +201,7 @@ function validate(command) {
if (typeof command.actor !== "string" || !command.actor) invalidCommand(command, "actor");
if (typeof command.session !== "string" || !command.session) invalidCommand(command, "session");
if (!Number.isSafeInteger(command.causal) || command.causal < 1) invalidCommand(command, "causal");
if (!Number.isSafeInteger(command.queuedAt) || command.queuedAt < 0) invalidCommand(command, "queuedAt");
if (command.id !== `${command.actor}:${command.causal}`) invalidCommand(command, "id");
if (command.kind !== "reorder_card") invalidCommand(command, "kind");
if (typeof command.cardId !== "string" || !command.cardId) invalidCommand(command, "cardId");
+44 -2
View File
@@ -21,6 +21,9 @@ let uploadsThisRun = 0;
let uploadedTotal = 0;
let inFlightUploads = 0;
let maxObservedInFlight = 0;
let acknowledgementStartedAt;
let conflictCount = 0;
let rejectionCount = 0;
let activeConflict;
let manualRetryCommand;
let stopped = false;
@@ -74,6 +77,7 @@ function migrateCommandLog(request, oldVersion) {
schemaVersion: COMMAND_SCHEMA,
targetColumn: command.targetColumn || "done",
accountPartition: command.accountPartition || "demo:demo",
queuedAt: Number.isSafeInteger(command.queuedAt) ? command.queuedAt : Date.now(),
});
}
meta.put({ from: oldVersion, to: DATABASE_VERSION, migrated: legacy.length }, MIGRATION_KEY);
@@ -158,6 +162,34 @@ function setPhase(phase, message) {
root.querySelector('[role="status"]').textContent = message;
}
function ageBucket(milliseconds) {
if (milliseconds < 1000) return "lt-1s";
if (milliseconds < 10000) return "1s-10s";
if (milliseconds < 60000) return "10s-1m";
return "gte-1m";
}
function latencyBucket(milliseconds) {
if (milliseconds < 50) return "lt-50ms";
if (milliseconds < 250) return "50ms-250ms";
if (milliseconds < 1000) return "250ms-1s";
return "gte-1s";
}
function publishDiagnostics(commands) {
const queued = Array.isArray(commands) ? commands : [];
const oldest = queued.reduce((value, command) => {
return Number.isSafeInteger(command.queuedAt) ? Math.min(value, command.queuedAt) : value;
}, Date.now());
root.setAttribute("data-sync-diag-queue-count", String(queued.length));
root.setAttribute("data-sync-diag-oldest-age-bucket", queued.length === 0 ? "empty" : ageBucket(Date.now() - oldest));
root.setAttribute("data-sync-diag-cursor", root.getAttribute("data-sync-ack-sequence") || "0");
root.setAttribute("data-sync-diag-conflicts", String(conflictCount));
root.setAttribute("data-sync-diag-rejections", String(rejectionCount));
const diagnostics = root.querySelector("[data-sync-diagnostics]");
diagnostics.textContent = `Queue ${queued.length}; oldest ${root.getAttribute("data-sync-diag-oldest-age-bucket")}; cursor ${root.getAttribute("data-sync-diag-cursor")}; acknowledgement ${root.getAttribute("data-sync-diag-ack-latency-bucket") || "none"}; conflicts ${conflictCount}; rejections ${rejectionCount}.`;
}
function validatePending(command) {
if (!command || command.schemaVersion !== COMMAND_SCHEMA || command.accountPartition !== accountPartition || command.kind !== "reorder_card" || typeof command.id !== "string" || !command.id || typeof command.cardId !== "string" || !command.cardId || command.targetColumn !== "done") {
throw new Error("invalid pending command");
@@ -301,6 +333,7 @@ function finishUpload() {
async function continuePendingWork() {
const commands = await pendingCommands(database);
root.setAttribute("data-sync-pending-count", String(commands.length));
publishDiagnostics(commands);
setExportAvailable(commands.length > 0);
if (commands.length === 0) return;
if (uploadsThisRun >= uploadLimit) {
@@ -340,6 +373,7 @@ async function synchronize(command) {
}
setOnline(true);
root.setAttribute("data-sync-upload-sequence", String(acknowledgement.serverSequence));
acknowledgementStartedAt = performance.now();
setPhase("awaiting-ack", `Command ${command.id} uploaded; awaiting canonical acknowledgement.`);
const reconnect = command.session || command.actor || "kanban";
@@ -371,9 +405,11 @@ async function synchronize(command) {
root.setAttribute("data-sync-pending-count", String(pendingAfterAck));
setExportAvailable(pendingAfterAck > 0);
root.setAttribute("data-sync-ack-sequence", String(canonical.serverSequence));
root.setAttribute("data-sync-ack-command-id", canonical.commandId);
root.setAttribute("data-sync-diag-cursor", String(canonical.serverSequence));
root.setAttribute("data-sync-diag-ack-latency-bucket", latencyBucket(performance.now() - acknowledgementStartedAt));
root.setAttribute("data-sync-canonical-column", canonical.canonicalColumn);
setPhase("acknowledged", `Command ${command.id} acknowledged in ${canonical.canonicalColumn}.`);
publishDiagnostics(await pendingCommands(database));
setPhase("acknowledged", `Queued change acknowledged in ${canonical.canonicalColumn}.`);
root.dispatchEvent(new CustomEvent("kanban:sync-acknowledged", { detail: canonical }));
synchronizing = false;
clearTimeout(leaseTimer);
@@ -410,7 +446,9 @@ async function synchronize(command) {
setPhase("rebased", `Canonical snapshot ${snapshot.serverSequence} already satisfies ${command.id}; committed and removed the pending command.`);
root.dispatchEvent(new CustomEvent("kanban:sync-rebased", { detail: { snapshot, command, decision } }));
} else {
conflictCount += 1;
activeConflict = { command, snapshot, decision };
publishDiagnostics(await pendingCommands(database));
setConflictResolutionAvailable(true);
setPhase("conflicted", `Canonical snapshot ${snapshot.serverSequence} conflicts with ${command.id} (${decision.reason}); the pending command remains queued.`);
root.dispatchEvent(new CustomEvent("kanban:sync-conflicted", { detail: { snapshot, command, decision } }));
@@ -431,6 +469,8 @@ async function synchronize(command) {
root.setAttribute("data-sync-error-status", String(error.status));
const remaining = await pendingCommands(database);
setManualRetryAvailable(false);
rejectionCount += 1;
publishDiagnostics(remaining);
if (command.conflictResolution === "keep-local-change") {
manualRetryCommand = undefined;
setConflictResolutionAvailable(true);
@@ -507,6 +547,8 @@ async function start() {
root.setAttribute("data-sync-in-flight", "0");
root.setAttribute("data-sync-max-observed-in-flight", "0");
root.setAttribute("data-sync-pending-count", String(commands.length));
publishDiagnostics(commands);
root.setAttribute("data-sync-diag-ack-latency-bucket", "none");
setExportAvailable(commands.length > 0);
setConflictResolutionAvailable(false);
setManualRetryAvailable(false);