From 0ae68d68426de2159288adb5968cd6f3124bf471 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 23:05:19 +0000 Subject: [PATCH 1/3] Family board: tapping a chore assigned to several kids opens the right kid's chore, and each kid has their own column Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01UaC9HmtHMdRP8dAbN4wAKj --- public/board.js | 10 +++++----- public/sw.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/board.js b/public/board.js index 89677f4..f2faa56 100644 --- a/public/board.js +++ b/public/board.js @@ -19,9 +19,9 @@ function draw() { $('#tiles').innerHTML = d.people.map(p => ``).join(''); $('#tiles').querySelectorAll('.tile').forEach(b => b.onclick = () => pick(d.people.find(p => p.id === b.dataset.id))); const day = (label, ds) => { const l = d.events.filter(e => e.allDay ? e.start <= ds && e.end > ds : ymd(new Date(e.start)) === ds).sort((a, b) => (b.allDay - a.allDay) || a.start - b.start); return `${label}` + (l.map(e => `
${esc(e.title)}${e.by ? ` ${esc(e.by)}` : ''}${e.allDay ? 'All day' : new Date(e.start).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
`).join('') || none('Nothing')); }; - const pill = (c, cls) => ``; - const pool = d.chores.filter(c => c.claimable && !c.done), per = {}; d.chores.filter(c => !(c.claimable && !c.done)).forEach(c => { (per[c.userName] = per[c.userName] || []).push(c); }); - const SHOW = 6, chores = (pool.length ? `` : '') + '
' + Object.keys(per).map(k => { const l = per[k], dn = l.filter(c => c.done).length, left = l.filter(c => !c.done); return `
${esc(k)}${dn} of ${l.length}
${left.map(c => pill(c)).join('')}${left.length ? '' : 'All done 🎉'}
`; }).join('') + '
'; + const pill = (c, cls) => ``; + const pool = d.chores.filter(c => c.claimable && !c.done), per = {}; d.chores.filter(c => !(c.claimable && !c.done)).forEach(c => { (per[c.user || c.userName] = per[c.user || c.userName] || []).push(c); }); + const SHOW = 6, chores = (pool.length ? `` : '') + '
' + Object.keys(per).map(k => { const l = per[k], dn = l.filter(c => c.done).length, left = l.filter(c => !c.done); return `
${esc(l[0].userName)}${dn} of ${l.length}
${left.map(c => pill(c)).join('')}${left.length ? '' : 'All done 🎉'}
`; }).join('') + '
'; $('#grid').innerHTML = card('Chores today', chores || none('No chores today'), '', 'wide') + card('Today and tomorrow', day('Today', t0) + day('Tomorrow', t1), 'event') + card('This week', weekCard(d.week)) + card('Shopping list', d.shopping.map(i => `
${esc(i.text)}${i.by ? `${esc(i.by)}` : ''}
`).join('') || none('Nothing to buy'), 'shopping') @@ -31,9 +31,9 @@ function draw() { } function poolAll() { const list = data.chores.filter(c => c.claimable && !c.done), { box, close } = sheet(`

Up for grabs

${list.length} chores. Tap one to pick it

${list.map(c => ``).join('')}

`); - box.querySelector('#no').onclick = close; box.querySelectorAll('.pill[data-c]').forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c))); + box.querySelector('#no').onclick = close; box.querySelectorAll('.pill[data-c]').forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c && (c.user || '') === (b.dataset.u || '')))); } -function wireChores() { document.querySelectorAll('[data-add]').forEach(b => b.onclick = () => addFlow(b.dataset.add)); const all = document.getElementById('poolall'); if (all) all.onclick = poolAll; document.querySelectorAll(".pill[data-c]").forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c))); } +function wireChores() { document.querySelectorAll('[data-add]').forEach(b => b.onclick = () => addFlow(b.dataset.add)); const all = document.getElementById('poolall'); if (all) all.onclick = poolAll; document.querySelectorAll(".pill[data-c]").forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c && (c.user || '') === (b.dataset.u || '')))); } async function load() { try { const r = await fetch('/api/board?day=' + ymd(new Date())); if (r.status === 403) { location.replace('/login'); return; } data = await r.json(); draw(); } catch {} } function pick(p) { diff --git a/public/sw.js b/public/sw.js index 38d05e2..e0eb9a2 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ // App-shell cache only; Seafile/API traffic always goes to the network. -const CACHE = 'hub-v134'; +const CACHE = 'hub-v135'; const KEEP = /^hub-(offline|share|who|data-)/; // caches that outlive a new version const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png']; From b0d6473e4d6d802b674aedc192abb8e41af285bc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 23:08:19 +0000 Subject: [PATCH 2/3] Visualizer test: recognise the player's new button layout Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01UaC9HmtHMdRP8dAbN4wAKj --- tools/visualizer-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/visualizer-test.js b/tools/visualizer-test.js index 85e7291..c04952d 100644 --- a/tools/visualizer-test.js +++ b/tools/visualizer-test.js @@ -10,6 +10,6 @@ for (const k of ['12345', 'radio:journeyfm']) ok(ctx.vizKind({ key: k }) === 'mu ok(ctx.VIZ_MODES.length === 6 && ctx.VIZ_MODES[0][0] === 'classic', 'six looks, Classic first'); const root = __dirname + '/../public/', rd = f => fs.readFileSync(root + f, 'utf8'); ok(rd('index.html').includes('/visualizer.js'), 'script is loaded'); -ok(rd('player.js').includes("id=\"npv\"") && rd('player.js').includes("id=\"cnv\""), 'buttons in the player and the Music card'); +ok(/id=\\"npv\\"|chip\('npv'/.test(rd('player.js')) && rd('player.js').includes("id=\"cnv\""), 'buttons in the player and the Music card'); ok(rd('player.js').includes('vizTrackChanged') && rd('radio-view.js').includes('vizTrackChanged'), 'follows track changes'); console.log('visualizer: ' + n + ' checks passed'); From 45c4dbe120ffc6366c2da2ed1a63e6698a22253f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 23:10:12 +0000 Subject: [PATCH 3/3] Family board: look for new chores every 15 seconds instead of every minute Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01UaC9HmtHMdRP8dAbN4wAKj --- public/board.js | 2 +- public/sw.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/board.js b/public/board.js index f2faa56..2c4fa27 100644 --- a/public/board.js +++ b/public/board.js @@ -49,7 +49,7 @@ function pick(p) { paint(); $('#no').onclick = close; box.querySelectorAll('.pad button').forEach(b => b.onclick = async () => { let k = b.dataset.k; $('#e').textContent = ''; if (k === '⌫') pin = pin.slice(0, -1); else if (k !== '✓' && pin.length < 8) { pin += k; if (p.pinLen && pin.length === p.pinLen) k = '✓'; } if (k === '✓') { try { await go(pin); return; } catch (e) { $('#e').textContent = e.message; pin = ''; } } paint(); }); } -clock(); setInterval(clock, 15000); load(); setInterval(() => { if (!document.hidden && $('#ov').hidden) load(); }, 60000); +clock(); setInterval(clock, 15000); load(); setInterval(() => { if (!document.hidden && $('#ov').hidden) load(); }, 15000); // (a chore someone just added shows up within a few seconds) document.addEventListener('visibilitychange', () => { if (!document.hidden) load(); }); // ---- tap a chore: (if it is up for grabs, choose what you are doing), pick your name, PIN if you are not a child, then it happens as you diff --git a/public/sw.js b/public/sw.js index e0eb9a2..e3071bd 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ // App-shell cache only; Seafile/API traffic always goes to the network. -const CACHE = 'hub-v135'; +const CACHE = 'hub-v136'; const KEEP = /^hub-(offline|share|who|data-)/; // caches that outlive a new version const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];