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}
-
-
+
+
{#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 {