feat(kanban): expose redacted sync diagnostics
req: operations/001 req: security/002 req: sync/016
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user