server auth

This commit is contained in:
2026-05-28 08:35:50 -04:00
parent 3d44a392f1
commit fc63f9c44a
37 changed files with 3877 additions and 74 deletions

View File

@@ -3,11 +3,107 @@
return;
}
const syncQueue = document.querySelector("[data-sync-queue]");
const syncQueueTitle = document.querySelector("[data-sync-queue-title]");
const syncQueueSummary = document.querySelector("[data-sync-queue-summary]");
const syncQueueList = document.querySelector("[data-sync-queue-list]");
const syncNow = document.querySelector("[data-sync-now]");
function normalizeDocPath(path) {
if (!path) return "";
return path.replace(/^\/+/, "");
}
function formatPath(path) {
return (path || "").replace(/^\/docs\//, "") || "/";
}
function pluralize(count, singular, plural) {
return count === 1 ? singular : plural;
}
function emitSyncState(detail) {
window.dispatchEvent(new CustomEvent("mdhub:sync-state", { detail: detail }));
}
function renderPendingEdits(pending, state) {
if (!syncQueue || !syncQueueTitle || !syncQueueSummary || !syncQueueList) {
return;
}
const conflicts = pending.filter(function (edit) {
return Boolean(edit.conflict);
});
const clean = pending.filter(function (edit) {
return !edit.conflict;
});
syncQueue.hidden = pending.length === 0;
if (pending.length === 0) {
syncQueueList.replaceChildren();
syncQueueTitle.textContent = "Synced";
syncQueueSummary.textContent = "No pending changes.";
if (syncNow) {
syncNow.disabled = true;
}
return;
}
const countLabel = pending.length + " pending " + pluralize(pending.length, "change", "changes");
syncQueueTitle.textContent = conflicts.length > 0 ? countLabel + " with conflicts" : countLabel;
if (!navigator.onLine) {
syncQueueSummary.textContent = "Offline. Changes will sync when the connection returns.";
} else if (state === "syncing") {
syncQueueSummary.textContent = "Syncing queued edits...";
} else if (conflicts.length > 0) {
syncQueueSummary.textContent = conflicts.length + " " + pluralize(conflicts.length, "file needs", "files need") + " conflict resolution.";
} else {
syncQueueSummary.textContent = "Queued edits are ready to sync.";
}
syncQueueList.replaceChildren();
pending.slice(0, 5).forEach(function (edit) {
const item = document.createElement("li");
item.dataset.state = edit.conflict ? "conflict" : "queued";
const path = document.createElement("span");
path.textContent = formatPath(edit.path);
item.appendChild(path);
const status = document.createElement("span");
status.textContent = edit.conflict ? "Conflict" : "Queued";
item.appendChild(status);
syncQueueList.appendChild(item);
});
if (pending.length > 5) {
const overflow = document.createElement("li");
overflow.textContent = "+" + (pending.length - 5) + " more";
syncQueueList.appendChild(overflow);
}
if (syncNow) {
syncNow.disabled = !navigator.onLine || clean.length === 0 || state === "syncing";
}
}
async function refreshSyncQueue(state) {
const pending = await window.MDHubCache.getPendingEdits();
pending.sort(function (a, b) {
return (b.updatedAt || 0) - (a.updatedAt || 0);
});
renderPendingEdits(pending, state || "idle");
emitSyncState({
state: state || "idle",
pending: pending.length,
conflicts: pending.filter(function (edit) {
return Boolean(edit.conflict);
}).length,
});
return pending;
}
async function postDocument(path, content, baseHash) {
const response = await fetch("/api/documents/" + normalizeDocPath(path), {
method: "POST",
@@ -40,6 +136,7 @@
try {
const result = await postDocument(path, content, baseHash);
await window.MDHubCache.deletePendingEdit(normalizedPath);
await refreshSyncQueue("saved");
return {
status: "saved",
result: result,
@@ -48,12 +145,14 @@
if (error.name === "DocumentConflictError") {
await window.MDHubCache.putPendingEdit(normalizedPath, content, baseHash);
await window.MDHubCache.markPendingEditConflict(normalizedPath, error.details);
await refreshSyncQueue("conflict");
return {
status: "conflict",
conflict: error.details,
};
}
await window.MDHubCache.putPendingEdit(normalizedPath, content, baseHash);
await refreshSyncQueue("queued");
return {
status: "queued",
};
@@ -62,15 +161,19 @@
async function syncPending() {
if (!navigator.onLine) {
await refreshSyncQueue("offline");
return;
}
const pending = await window.MDHubCache.getPendingEdits();
const pending = await refreshSyncQueue("syncing");
pending.sort(function (a, b) {
return (a.updatedAt || 0) - (b.updatedAt || 0);
});
for (const edit of pending) {
if (edit.conflict) {
continue;
}
try {
await postDocument(edit.path.replace(/^\/docs\//, ""), edit.content, edit.baseHash);
await window.MDHubCache.deletePendingEdit(edit.path);
@@ -81,16 +184,31 @@
break;
}
}
await refreshSyncQueue("idle");
}
window.addEventListener("online", function () {
syncPending().catch(function () {});
});
window.addEventListener("offline", function () {
refreshSyncQueue("offline").catch(function () {});
});
if (syncNow) {
syncNow.addEventListener("click", function () {
syncPending().catch(function () {
refreshSyncQueue("queued").catch(function () {});
});
});
}
syncPending().catch(function () {});
refreshSyncQueue("idle").catch(function () {});
window.MDHubSync = {
saveDocument: saveDocument,
syncPending: syncPending,
refreshSyncQueue: refreshSyncQueue,
};
})();