From 29d50985df1b30ee5fa8e6f4b4d3f84651c67d3f Mon Sep 17 00:00:00 2001 From: JCEEE <0xjceee@proton.me> Date: Tue, 15 Sep 2026 09:05:13 +0100 Subject: [PATCH] fix dem bugs --- frontend/src/lib/components/TopNav.svelte | 6 +- frontend/src/lib/stores/chat.svelte.ts | 11 +- frontend/src/lib/stores/fam.svelte.ts | 16 ++- .../src/lib/stores/shared-device.svelte.ts | 60 ++++++++++ frontend/src/lib/templateIcons.ts | 12 +- frontend/src/routes/[fam]/+layout.svelte | 105 +----------------- frontend/src/routes/[fam]/+page.svelte | 64 ++++++++++- .../[fam]/[username]/chores/+page.svelte | 28 ++++- 8 files changed, 181 insertions(+), 121 deletions(-) create mode 100644 frontend/src/lib/stores/shared-device.svelte.ts diff --git a/frontend/src/lib/components/TopNav.svelte b/frontend/src/lib/components/TopNav.svelte index 82d297c..baaa425 100644 --- a/frontend/src/lib/components/TopNav.svelte +++ b/frontend/src/lib/components/TopNav.svelte @@ -1,19 +1,16 @@ @@ -22,7 +19,6 @@ {#if role} {role} {/if} - {@render left?.()} this.unsubs.push(unsub)) - .catch((err: Error) => console.error('[chatStore] messages subscribe failed:', err)); + .catch((err: Error) => { + console.error('[chatStore] messages subscribe failed:', err); + famStore.connectionDown = true; + }); const typingSub = pb .collection('chat_typing') @@ -128,7 +132,10 @@ class ChatStore { this.onTyping(data.action, data.record); }) .then((unsub) => this.unsubs.push(unsub)) - .catch((err: Error) => console.error('[chatStore] typing subscribe failed:', err)); + .catch((err: Error) => { + console.error('[chatStore] typing subscribe failed:', err); + famStore.connectionDown = true; + }); await Promise.allSettled([msgSub, typingSub]); } diff --git a/frontend/src/lib/stores/fam.svelte.ts b/frontend/src/lib/stores/fam.svelte.ts index 216da0d..707441e 100644 --- a/frontend/src/lib/stores/fam.svelte.ts +++ b/frontend/src/lib/stores/fam.svelte.ts @@ -203,8 +203,11 @@ class FamStore { this.bonusTemplates = bonusTemplatesRes; this.rewards = rewardsRes; this.seasons = seasonsRes; + // A completed resync proves the pipe is healthy again. + this.connectionDown = false; } catch (e) { console.error('FamStore.resync failed:', e); + if (!this.destroyed) this.connectionDown = true; } finally { this.resyncing = false; } @@ -228,7 +231,10 @@ class FamStore { .then((unsub) => { this.connWatchers.push(unsub); }) - .catch(() => {}); + .catch(() => { + // We never even got the connect signal — surface it. + if (!this.destroyed) this.connectionDown = true; + }); const onDisc = pb.realtime.onDisconnect; pb.realtime.onDisconnect = (active: string[]) => { try { @@ -238,9 +244,10 @@ class FamStore { if (active.length > 0 && !this.destroyed) this.connectionDown = true; }; // Browser-level: sleep/wake and offline/online can kill SSE without the - // SDK noticing promptly — resync when the network/tab comes back. + // SDK noticing promptly. Don't touch the flag here — resync() sets it + // from the actual outcome (clearing it early is what left the dot + // green while the console showed errors). const onOnline = () => { - this.connectionDown = false; this.resync().catch(() => {}); }; const onVisible = () => { @@ -284,6 +291,9 @@ class FamStore { }) .catch((err: Error) => { console.error(`[famStore] subscribe failed for ${collection}:`, err); + // A failed subscribe IS the outage the console shows while + // the dot stayed green — flag it; PB_CONNECT/resync clears it. + if (!this.destroyed) this.connectionDown = true; }); }); diff --git a/frontend/src/lib/stores/shared-device.svelte.ts b/frontend/src/lib/stores/shared-device.svelte.ts new file mode 100644 index 0000000..9dbfd6f --- /dev/null +++ b/frontend/src/lib/stores/shared-device.svelte.ts @@ -0,0 +1,60 @@ +import { isSharedDevice, setSharedDevice } from '$lib/client/shared-device'; +import { notices } from './notices.svelte'; + +// Per-device shared-computer mode (NOT a fam setting — see +// lib/client/shared-device.ts). One reactive home for the flag so the fam +// layout (switcher + idle-lock gating) and the dashboard hero toggle stay in +// sync. Anyone holding the device can flip it; the server sees the cookie +// mirror as data.sharedDevice. +class SharedDeviceStore { + on = $state( + typeof localStorage !== 'undefined' ? isSharedDevice() : false + ); + + async toggle() { + this.on = !this.on; + setSharedDevice(this.on); + if (!this.on) return; + // Just enabled — remind about PIN state. Kids without a PIN can't be + // picked until one is set (picker shows "ask a parent"); kids with one + // get a nudge to remember it. Values never exposed — only set/unset. + try { + const res = await fetch('/api/pins/status'); + if (!res.ok) throw new Error(); + const s = await res.json(); + if (Array.isArray(s.children)) { + // Parent view: roster of who still needs a PIN. + const missing = s.children.filter((c: any) => !c.hasPin).map((c: any) => c.name); + const ready = s.children.filter((c: any) => c.hasPin).map((c: any) => c.name); + if (missing.length > 0) { + notices.warning( + 'Shared mode on', + `Still need a PIN: ${missing.join(', ')} (set in Settings → Members).` + + (ready.length > 0 ? ` Ready: ${ready.join(', ')}.` : '') + ); + } else { + notices.success( + 'Shared mode on', + `PINs ready for ${ready.length > 0 ? ready.join(', ') : 'everyone'}. Remind the kids!` + ); + } + } else if (typeof s.hasPin === 'boolean') { + // Child view: only their own state. + if (s.hasPin) { + notices.success('Shared mode on', 'Remember your 3-digit PIN to switch back in!'); + } else { + notices.warning( + 'Shared mode on', + 'You need a PIN first. Ask a parent to set one up.' + ); + } + } else { + notices.info('Shared mode on', 'This computer now uses profile PINs.'); + } + } catch { + notices.info('Shared mode on', 'This computer now uses profile PINs.'); + } + } +} + +export const sharedDevice = new SharedDeviceStore(); diff --git a/frontend/src/lib/templateIcons.ts b/frontend/src/lib/templateIcons.ts index 65536de..a35b5c4 100644 --- a/frontend/src/lib/templateIcons.ts +++ b/frontend/src/lib/templateIcons.ts @@ -20,7 +20,11 @@ import { PawPrint, Guitar, Medal, - Wrench + Wrench, + PenLine, + SprayCan, + Droplets, + CookingPot } from '@lucide/svelte'; export const ICON_MAP: Record = { @@ -45,7 +49,11 @@ export const ICON_MAP: Record = { PawPrint, Guitar, Medal, - Wrench + Wrench, + PenLine, + SprayCan, + Droplets, + CookingPot }; export const ICON_NAMES = Object.keys(ICON_MAP); diff --git a/frontend/src/routes/[fam]/+layout.svelte b/frontend/src/routes/[fam]/+layout.svelte index f7c2770..34fc626 100644 --- a/frontend/src/routes/[fam]/+layout.svelte +++ b/frontend/src/routes/[fam]/+layout.svelte @@ -7,10 +7,10 @@ import { chatStore } from '$lib/stores/chat.svelte'; import { notices } from '$lib/stores/notices.svelte'; import { Sidebar, TopNav, Footer, Chat, SharedPicker } from '$lib/components'; - import { chatIcon, monitorIcon } from '$lib/components/icons'; + import { chatIcon } from '$lib/components/icons'; import { recordShortcut } from '$lib/shortcut'; import { installLockTracking, lockDue } from '$lib/client/lock'; - import { isSharedDevice, setSharedDevice } from '$lib/client/shared-device'; + import { sharedDevice } from '$lib/stores/shared-device.svelte'; import { themeShades } from '$lib/theme'; import '$lib/theme-patterns.css'; import { themeDraft } from '$lib/stores/theme.svelte'; @@ -44,59 +44,13 @@ .map((m: any) => ({ id: m.id, name: m.name, color: m.color, username: m.username })) ); - // Shared-computer mode (per-device flag, NOT a fam setting — see - // lib/client/shared-device.ts). Makes the PIN system functional on this - // browser: profile switcher + idle lock. Anyone logged in (or not) can - // flip it; server sees the cookie mirror as data.sharedDevice. - let sharedOn = $state( - typeof localStorage !== 'undefined' ? isSharedDevice() : !!(data as any).sharedDevice - ); - let sharedToast = $state(''); - - async function toggleShared() { - sharedOn = !sharedOn; - setSharedDevice(sharedOn); - if (!sharedOn) { - sharedToast = ''; - return; - } - // Just enabled — remind about PIN state. Kids without a PIN can't be - // picked until one is set (picker shows "ask a parent"); kids with one - // get a nudge to remember it. Values never exposed — only set/unset. - try { - const res = await fetch('/api/pins/status'); - if (!res.ok) throw new Error(); - const s = await res.json(); - if (Array.isArray(s.children)) { - // Parent view: roster of who still needs a PIN. - const missing = s.children.filter((c: any) => !c.hasPin).map((c: any) => c.name); - const ready = s.children.filter((c: any) => c.hasPin).map((c: any) => c.name); - sharedToast = - missing.length > 0 - ? `Shared mode on — still need a PIN: ${missing.join(', ')} (set in Settings → Members).` + - (ready.length > 0 ? ` Ready: ${ready.join(', ')}.` : '') - : `Shared mode on — PINs ready for ${ready.length > 0 ? ready.join(', ') : 'everyone'}. Remind the kids!`; - } else if (typeof s.hasPin === 'boolean') { - // Child view: only their own state. - sharedToast = s.hasPin - ? 'Shared mode on — remember your 3-digit PIN to switch back in!' - : 'Shared mode on — you need a PIN first. Ask a parent to set one up.'; - } else { - sharedToast = 'Shared mode on for this computer.'; - } - } catch { - sharedToast = 'Shared mode on for this computer.'; - } - setTimeout(() => (sharedToast = ''), 8000); - } - // Idle lock (children on shared devices only): return to the picker after // `lockMins` of no interaction. localStorage-backed (see lib/client/lock.ts) // so a closed browser still trips the lock on next launch. $effect(() => { const isChild = data.session?.role === 'child'; const lockMins = Number(data.lockMins) || 0; - if (!isChild || !sharedOn || lockMins <= 0 || data.picker) return; + if (!isChild || !sharedDevice.on || lockMins <= 0 || data.picker) return; installLockTracking(); if (lockDue(lockMins)) pickerOpen = true; if (!lockTimer) { @@ -306,25 +260,13 @@ announcement={disabled && !activating ? accessMessage(accessReason, isParent) : ''} connected={!famStore.connectionDown} > - {#snippet left()} - - {/snippet} - {#if data.session && sharedOn && (data.deviceChildIds?.length || 0) > 0} + {#if data.session && sharedDevice.on && (data.deviceChildIds?.length || 0) > 0} + @@ -496,6 +526,36 @@ letter-spacing: 0.08em; opacity: 0.85; } + /* Shared-computer toggle row, bottom-left of the hero */ + .vh-foot { + flex: 1 1 100%; + display: flex; + justify-content: flex-start; + margin-top: 0.75rem; + } + .shared-hero-toggle { + display: inline-flex; + align-items: center; + gap: 0.45rem; + background: rgba(255, 255, 255, 0.16); + color: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(255, 255, 255, 0.35); + border-radius: 999px; + padding: 0.4rem 0.95rem; + font-size: 0.8rem; + font-weight: 700; + cursor: pointer; + } + .shared-hero-toggle:hover { + background: rgba(255, 255, 255, 0.28); + color: #fff; + } + .shared-hero-toggle.on { + background: #fff; + color: #4338ca; + border-color: transparent; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + } /* ── Chores card totals ── */ .chores-totals { diff --git a/frontend/src/routes/[fam]/[username]/chores/+page.svelte b/frontend/src/routes/[fam]/[username]/chores/+page.svelte index c3ba61b..4f727dc 100644 --- a/frontend/src/routes/[fam]/[username]/chores/+page.svelte +++ b/frontend/src/routes/[fam]/[username]/chores/+page.svelte @@ -242,10 +242,11 @@ showTodoModal = true; } - function openCreateChore() { - assignModal = { t: null as any, memberId: members[0]?.id || '' }; + function openCreateChore(memberId = '') { + const target = memberId || members[0]?.id || ''; + assignModal = { t: null as any, memberId: target }; assignStep = 1; - assignTarget = 'member'; + assignTarget = target ? 'member' : 'shared'; assignName = ''; assignDescription = ''; assignColor = ''; @@ -390,12 +391,14 @@ {/if} - - + +

Templates — Drag to assign

- +
{#each templates as t} @@ -607,6 +610,9 @@ {#if assignedForMember(m.id).length === 0}

Drop a chore here

{/if} +
{/if}
@@ -690,6 +696,9 @@ {:else}

Drop a chore here

{/each} + {/if} @@ -1193,6 +1202,12 @@ .template-list .card.template { width: 100%; } + /* Below desktop there's no drag-and-drop — hide the templates strip + (each member/shared card carries its own Create button instead). + :global because the class lives on the Card component's root. */ + :global(.chores-templates-card) { + display: none; + } } /* ── Kanban (3 columns, scrolls via Card scrollX) ── */ .kanban { @@ -1266,6 +1281,7 @@ .add-inline:hover { border-color: #6366f1; background: #eef2ff; + color: #4338ca; } .card {