refactor(frontend): one account per browser, its local state under plain keys

The per-tab account pin, the X-Siftlode-Account header and ?account= go: the session cookie alone
says who is asking. localStorage keys lose their account suffix; the browser records whose state it
holds (siftlode.owner), BootGate hands it to the account the server answered as, and a different
account starts from a clean slate (the public /watch keys stay). The first boot moves the account's
old per-account copies onto the plain keys and drops other accounts' copies. Until the claim, and
after another tab claimed the browser for someone else, the account helpers read and write nothing.

The account panel lists no other accounts: "Add another account" becomes "Sign in as someone else"
(Google's account chooser; the pick replaces this account, a cancel keeps it), shown where Google
sign-in is configured.
This commit is contained in:
2026-10-20 15:29:41 +02:00
parent 6645caa157
commit d6dada7fe5
51 changed files with 672 additions and 559 deletions
+6 -18
View File
@@ -1,7 +1,7 @@
// Download-center domain: the yt-dlp job lifecycle, per-user profiles, share links/recipients, the
// phase-2 editor, storyboards, and the admin storage/quota views. Self-contained — its types only
// reference each other, and its methods only touch the shared `req`/`getActiveAccount` from core.
import { beacon, req, getActiveAccount } from "./core";
// reference each other, and its methods only touch the shared `req`/`beacon` from core.
import { beacon, req } from "./core";
export interface DownloadSpec {
mode: "av" | "v" | "a"; // audio+video | video-only | audio-only
@@ -314,22 +314,10 @@ export const downloadsApi = {
},
onSettled,
),
// Filmstrip sprite (an <img>/background src → direct nav, so it needs the ?account= wallet gate).
storyboardImageUrl: (id: number): string => {
const a = getActiveAccount();
return a != null
? `/api/downloads/${id}/storyboard.jpg?account=${a}`
: `/api/downloads/${id}/storyboard.jpg`;
},
// The locally-generated poster (an <img> src → direct nav, so same ?account= wallet gate). Without
// it the poster resolves to the session-default account and 404s for a per-tab account's download —
// which reads as a blank Library thumbnail once the poster is the card's primary image.
posterImageUrl: (id: number): string => {
const a = getActiveAccount();
return a != null
? `/api/downloads/${id}/poster.jpg?account=${a}`
: `/api/downloads/${id}/poster.jpg`;
},
// Filmstrip sprite (an <img>/background src).
storyboardImageUrl: (id: number): string => `/api/downloads/${id}/storyboard.jpg`,
// The locally-generated poster (an <img> src).
posterImageUrl: (id: number): string => `/api/downloads/${id}/poster.jpg`,
// admin
adminDownloads: (): Promise<DownloadJob[]> => req("/api/admin/downloads"),