| index b6f1889..e396d33 100644 |
| --- a/home/web/static/home.js |
| +++ b/home/web/static/home.js |
| @@ -702,6 +702,7 @@ function openArchive() { |
| archiving = null; |
| $("vault").classList.add("hidden"); |
| $("archive-stern").classList.add("hidden"); |
| + $("archive-checking").classList.add("hidden"); |
| $("archive-main").classList.remove("hidden"); |
| $("archive").classList.remove("hidden"); |
| archiveStatus(""); |
| @@ -718,6 +719,7 @@ function closeArchive() { |
| function closeArchiveStern() { |
| archiving = null; |
| $("archive-stern").classList.add("hidden"); |
| + $("archive-checking").classList.add("hidden"); |
| $("archive-main").classList.remove("hidden"); |
| archiveStatus(""); |
| } |
| @@ -778,12 +780,21 @@ function unarchiveSpace(s) { |
| persistVault(archiveStatus); |
| } |
| |
| +// The stern step assembles BEFORE it shows: both checks (ownership → destroy |
| +// zone, last-admin → hold) run first, so pieces never pop into a visible panel. |
| +// A fast answer shows no loader at all; past STERN_LOADER_DELAY_MS the spinner |
| +// appears, and at STERN_CHECK_CAP_MS we reveal with whatever has settled — a |
| +// straggler still applies live (rare, and correctness beats stillness). |
| +const STERN_LOADER_DELAY_MS = 150; |
| +const STERN_CHECK_CAP_MS = 2500; |
| +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); |
| + |
| // openArchiveDelete opens the stern permanent-delete step for one archived space. |
| // "Delete your access" (drop the saved entry) is offered unless the space reports |
| // this is its ONLY admin key (checkSoleAdmin); for a space THIS account created, |
| // the deeper "destroy for everyone" is revealed too (ownership checked against |
| // the provisioner ledger; the server re-enforces it on delete). |
| -function openArchiveDelete(s) { |
| +async function openArchiveDelete(s) { |
| archiving = s; |
| sternSoleAdmin = false; |
| sternDestroyable = false; |
| @@ -797,22 +808,39 @@ function openArchiveDelete(s) { |
| $("destroy-zone").classList.add("hidden"); |
| $("destroy-forever").disabled = true; |
| $("archive-main").classList.add("hidden"); |
| - $("archive-stern").classList.remove("hidden"); |
| + $("archive-stern").classList.add("hidden"); |
| + $("archive-checking").classList.add("hidden"); |
| archiveStatus(""); |
| - if (spacesEnabled) { |
| - apiJSON("/api/spaces", { headers: { Authorization: "Bearer " + state.token } }) |
| - .then((r) => { |
| - const mine = (r.spaces || []).some((sp) => sp.host === host); |
| - if (mine && archiving === s) { |
| - sternDestroyable = true; |
| - $("destroy-zone").classList.remove("hidden"); |
| - prefetchLogin(); // the destroy ceremony needs fresh options (Safari gesture rule) |
| - updateSternHold(); // the note steers to destroy once it's on the table |
| - } |
| - }) |
| - .catch(() => { /* can't confirm ownership — don't offer the destroy */ }); |
| + |
| + const checks = Promise.allSettled([checkOwnership(s, host), checkSoleAdmin(s)]); |
| + const fast = await Promise.race([checks.then(() => true), sleep(STERN_LOADER_DELAY_MS).then(() => false)]); |
| + if (archiving !== s) return; // backed out mid-check |
| + if (!fast) { |
| + $("archive-checking").classList.remove("hidden"); |
| + await Promise.race([checks, sleep(STERN_CHECK_CAP_MS)]); |
| + if (archiving !== s) return; |
| + $("archive-checking").classList.add("hidden"); |
| } |
| - checkSoleAdmin(s); |
| + updateSternHold(); |
| + $("archive-stern").classList.remove("hidden"); |
| +} |
| + |
| +// checkOwnership learns whether THIS account created the space (the provisioner |
| +// ledger), which puts "destroy for everyone" on the table. Rendering happens in |
| +// updateSternHold — this only sets the flag, so it composes with the pre-reveal |
| +// assembly above and still applies live if it straggles past the cap. |
| +function checkOwnership(s, host) { |
| + if (!spacesEnabled) return Promise.resolve(); |
| + return apiJSON("/api/spaces", { headers: { Authorization: "Bearer " + state.token } }) |
| + .then((r) => { |
| + const mine = (r.spaces || []).some((sp) => sp.host === host); |
| + if (mine && archiving === s) { |
| + sternDestroyable = true; |
| + prefetchLogin(); // the destroy ceremony needs fresh options (Safari gesture rule) |
| + updateSternHold(); |
| + } |
| + }) |
| + .catch(() => { /* can't confirm ownership — don't offer the destroy */ }); |
| } |
| |
| // checkSoleAdmin asks the space itself — with the saved session token, straight |
| @@ -823,10 +851,10 @@ function openArchiveDelete(s) { |
| // on a positive answer. |
| function checkSoleAdmin(s) { |
| const token = s.credentials && s.credentials.token; |
| - if (!token) return; |
| + if (!token) return Promise.resolve(); |
| let url; |
| - try { url = new URL("/api/me?token=" + encodeURIComponent(token), s.url); } catch { return; } |
| - fetch(url, { signal: AbortSignal.timeout(8000) }) |
| + try { url = new URL("/api/me?token=" + encodeURIComponent(token), s.url); } catch { return Promise.resolve(); } |
| + return fetch(url, { signal: AbortSignal.timeout(8000) }) |
| .then((r) => (r.ok ? r.json() : null)) |
| .then((me) => { |
| if (soleAdminHold(me) && archiving === s) { |
| @@ -837,10 +865,12 @@ function checkSoleAdmin(s) { |
| .catch(() => { /* no answer — no hold */ }); |
| } |
| |
| -// updateSternHold renders the last-admin hold: "Delete your access" goes (and |
| -// stays) off, and the note says why and what to do instead — destroy the space |
| -// below when this account created it, otherwise hand admin to someone else first. |
| +// updateSternHold renders what the checks learned: the destroy zone when this |
| +// account owns the space, and the last-admin hold — "Delete your access" goes |
| +// (and stays) off, with the note saying why and what to do instead: destroy the |
| +// space below, or hand admin to someone else first. |
| function updateSternHold() { |
| + $("destroy-zone").classList.toggle("hidden", !sternDestroyable); |
| const note = $("sole-admin-note"); |
| if (!sternSoleAdmin) { note.classList.add("hidden"); return; } |
| note.textContent = sternDestroyable |