From 9566506d30906473ac150f88e85d02e3502fc3f0 Mon Sep 17 00:00:00 2001
From: JCEEE <0xjceee@proton.me>
Date: Wed, 16 Sep 2026 21:07:21 +0100
Subject: [PATCH] fix auth issues
---
frontend/src/routes/[fam]/+layout.server.ts | 9 ++-
frontend/src/routes/[fam]/+layout.svelte | 58 +++++++++++++++++++
.../src/routes/[fam]/[username]/+page.svelte | 4 ++
frontend/src/routes/login/+page.server.ts | 17 +++++-
4 files changed, 85 insertions(+), 3 deletions(-)
diff --git a/frontend/src/routes/[fam]/+layout.server.ts b/frontend/src/routes/[fam]/+layout.server.ts
index 3a2e12d..66b8dbd 100644
--- a/frontend/src/routes/[fam]/+layout.server.ts
+++ b/frontend/src/routes/[fam]/+layout.server.ts
@@ -134,7 +134,14 @@ export async function load(event) {
chat = await resolveChatIdentity(session, pbToken);
// fams is superadmin-only (non-realtime). Fetched server-side for both
// roles; also recomputes + persists the derived `active` flag.
- const res = await ensureFamAccess(famId).catch(() => null);
+ // Retried: a post-deploy PB blip here blanks `fam`, which both hides
+ // data AND disables the canonical slug redirect below (letting /{id}
+ // URLs persist).
+ let res: any = null;
+ for (let i = 0; i < 3 && !res; i++) {
+ if (i > 0) await new Promise((r) => setTimeout(r, 400 * i));
+ res = await ensureFamAccess(famId).catch(() => null);
+ }
if (res) {
fam = res.fam;
famAccess = res.access;
diff --git a/frontend/src/routes/[fam]/+layout.svelte b/frontend/src/routes/[fam]/+layout.svelte
index 34fc626..59a3579 100644
--- a/frontend/src/routes/[fam]/+layout.svelte
+++ b/frontend/src/routes/[fam]/+layout.svelte
@@ -78,6 +78,14 @@
// Settings stays usable while paused so admins can apply a code / manage billing.
let hasAuth = $derived(!!data.session);
let locked = $derived(disabled && !page.url.pathname.endsWith('/settings'));
+ // Expired-token zombie state: public PB rules still let logged-out users
+ // READ everything, so the dashboard looks alive while every write fails.
+ // Say so explicitly (join/switch/picker flows render their own UI).
+ let showLoggedOut = $derived(
+ !data.session &&
+ !pickerMode &&
+ !['join', 'switch'].includes(page.url.pathname.split('/')[2] || '')
+ );
// ── Post-checkout activation (event-driven) ──
// Landing with ?checkout=return: if the webhook has already landed we show
@@ -282,6 +290,28 @@
{@render children()}
+ {#if showLoggedOut}
+
+
+
You're logged out
+
+ Your session expired, so everything below is read-only — toggles and
+ claims won't work until you log back in.
+
+
+ Parent log in
+ {#if page.params.username}
+ Rejoin as {page.params.username}
+ {/if}
+
+
Kids: re-open your invite link, or ask a parent to send it again.
+
+
+ {/if}
{#if locked}
@@ -482,6 +512,34 @@
color: #6b7280;
font-size: 0.9rem;
}
+ /* Logged-out card must be clickable (the paused overlay is display-only). */
+ .loggedout-overlay {
+ pointer-events: auto;
+ }
+ .loggedout-actions {
+ display: flex;
+ gap: 0.6rem;
+ justify-content: center;
+ margin-top: 0.4rem;
+ }
+ .loggedout-actions a {
+ border-radius: 8px;
+ padding: 0.5rem 1.1rem;
+ font-size: 0.85rem;
+ font-weight: 700;
+ text-decoration: none;
+ }
+ .loggedout-actions .btn-primary {
+ background: #6366f1;
+ color: #fff;
+ }
+ .loggedout-actions .btn-secondary {
+ background: #eef2ff;
+ color: #4338ca;
+ }
+ .disabled-card .hint {
+ font-size: 0.78rem;
+ }
.chat-toggle {
position: relative;
width: 40px;
diff --git a/frontend/src/routes/[fam]/[username]/+page.svelte b/frontend/src/routes/[fam]/[username]/+page.svelte
index 72ff63a..a05408b 100644
--- a/frontend/src/routes/[fam]/[username]/+page.svelte
+++ b/frontend/src/routes/[fam]/[username]/+page.svelte
@@ -847,6 +847,10 @@
async function toggle(chore: AssignedChore) {
if (accessDisabled) return;
+ if (!pbToken) {
+ showToast('Logged out — please log in again to update chores.');
+ return;
+ }
if (chore.isTodo && isTodoExpired(chore)) return;
if (togglingIds.has(chore.id)) return;
togglingIds = new Set(togglingIds).add(chore.id);
diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts
index 9434447..b4add95 100644
--- a/frontend/src/routes/login/+page.server.ts
+++ b/frontend/src/routes/login/+page.server.ts
@@ -30,8 +30,21 @@ export const actions = {
// An explicit email/password login supersedes kid mode on a shared device.
clearActiveChild(event.cookies);
- const fam = await pbAdmin.getOne('fams', user.famId).catch(() => null);
- const famSlug = fam?.slug || user.famId;
+ // Fam lookup with retries: right after a deploy/rebuild PB can blip and
+ // a single failed fetch falls back to the raw fam ID in the URL
+ // (/{famId}/...), which then persists via bookmarks/shortcuts.
+ let fam: any = null;
+ for (let i = 0; i < 4 && !fam; i++) {
+ if (i > 0) await new Promise((r) => setTimeout(r, 500 * i));
+ fam = await pbAdmin.getOne('fams', user.famId).catch(() => null);
+ }
+ if (!fam) {
+ return fail(503, {
+ error: 'Family data is briefly unavailable (server restarting). Please try again.',
+ email
+ });
+ }
+ const famSlug = fam.slug;
// Parents (admins) land on the fam dashboard — no username in the URL.
if (user.role === 'parent') {