feat(kanban): reauthorize queued replay

req: sync/019

req: security/004

req: auth/005

req: operations/002
This commit is contained in:
slhx agent
2026-07-13 18:18:52 +02:00
parent a17de85b4f
commit 6de4dcb73b
4 changed files with 366 additions and 17 deletions
+19 -7
View File
@@ -23,11 +23,12 @@ let maxObservedInFlight = 0;
let stopped = false;
class UploadError extends Error {
constructor(status, retryable, reason) {
constructor(status, retryable, kind, reason) {
super(`sync upload failed with ${status}`);
this.name = "UploadError";
this.status = status;
this.retryable = retryable;
this.kind = kind;
this.reason = reason;
}
}
@@ -190,8 +191,9 @@ async function upload(command) {
}
if (!response.ok) {
const problem = await response.json().catch(() => ({}));
const kind = typeof problem.kind === "string" ? problem.kind : "unclassified-rejection";
const reason = typeof problem.error === "string" ? problem.error : "unclassified rejection";
throw new UploadError(response.status, response.status >= 500, reason);
throw new UploadError(response.status, response.status >= 500, kind, reason);
}
return response.json();
} catch (error) {
@@ -282,6 +284,7 @@ async function synchronize(command) {
root.setAttribute("data-sync-uploaded-total", String(uploadedTotal));
root.setAttribute("data-sync-pending-count", String((await pendingCommands(database)).length));
root.setAttribute("data-sync-ack-sequence", String(canonical.serverSequence));
root.setAttribute("data-sync-ack-command-id", canonical.commandId);
root.setAttribute("data-sync-canonical-column", canonical.canonicalColumn);
setPhase("acknowledged", `Command ${command.id} acknowledged in ${canonical.canonicalColumn}.`);
root.dispatchEvent(new CustomEvent("kanban:sync-acknowledged", { detail: canonical }));
@@ -324,14 +327,23 @@ async function synchronize(command) {
setOnline(true);
clearTimeout(leaseTimer);
root.setAttribute("data-sync-error", error.message);
root.setAttribute("data-sync-error-kind", "permanent-rejection");
root.setAttribute("data-sync-error-status", String(error.status));
root.setAttribute("data-sync-error-reason", error.reason);
root.setAttribute("data-sync-rejected-command-id", command.id);
const remaining = await pendingCommands(database);
root.setAttribute("data-sync-pending-count", String(remaining.length));
setManualRetryAvailable(false);
setPhase("rejected", `Command ${command.id} was permanently rejected (${error.status}: ${error.reason}); ${remaining.length} durable command${remaining.length === 1 ? " remains" : "s remain"} queued for review.`);
if (error.kind === "authorization-denial") {
root.setAttribute("data-sync-error-kind", "authorization-denial");
root.setAttribute("data-sync-pending-count", "redacted");
root.setAttribute("data-sync-redacted-pending", "true");
root.removeAttribute("data-sync-error-reason");
root.removeAttribute("data-sync-rejected-command-id");
setPhase("authorization-denied", "Current session cannot access local queued work. Sign back into the owning account to continue.");
} else {
root.setAttribute("data-sync-error-kind", "permanent-rejection");
root.setAttribute("data-sync-error-reason", error.reason);
root.setAttribute("data-sync-rejected-command-id", command.id);
root.setAttribute("data-sync-pending-count", String(remaining.length));
setPhase("rejected", `Command ${command.id} was permanently rejected (${error.status}: ${error.reason}); ${remaining.length} durable command${remaining.length === 1 ? " remains" : "s remain"} queued for review.`);
}
await releaseUploaderLease(database);
root.setAttribute("data-sync-leader", "false");
return;