Parental controls: approvals lift bedtime, bedtime follows the child's clock, urgent push + banner + reminders for requests, reward cap raised to 60
build-container / build (push) Successful in 4m3s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-29 08:29:47 -04:00
1 parent d0e4e82e0a
commit 0a33119807
11 files changed
+59 -19

No files matched your search

+16 -1
View File
@@ -408,9 +408,24 @@ const kidUi = { st: null, timer: null, told: new Set() };
async function kidStart() {
if (!ME || !ME.kid || !ME.kid.on || kidUi.timer) return;
kidAskButton();
const tick = async () => { try { kidUi.st = document.visibilityState === 'visible' ? await jfetch('/api/kid/ping', { method: 'POST', body: JSON.stringify({ area: kidArea() }) }) : await jfetch('/api/kid/status'); } catch (e) { return; } kidPaint(); };
const tick = async () => { try { kidUi.st = document.visibilityState === 'visible' ? await jfetch('/api/kid/ping', { method: 'POST', body: JSON.stringify({ area: kidArea(), tz: Intl.DateTimeFormat().resolvedOptions().timeZone }) }) : await jfetch('/api/kid/status'); } catch (e) { return; } kidPaint(); };
kidUi.tick = tick; await tick(); kidUi.timer = setInterval(tick, 60e3); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') tick(); });
}
// ---- A parent: a pill at the top of every screen while a child is asking (with a beep and a title count), so a request is never missed. Tap it to answer.
const askWatch = { n: 0, timer: null, tick: null };
async function askWatchStart() {
if (!ME || ME.role !== 'admin' || askWatch.timer) return; const base = document.title.replace(/^\(\d+\) /, '');
const tick = async () => {
let r; try { r = await jfetch('/api/kid/requests'); } catch { return; }
const open = (r.requests || []).filter(x => x.status === 'open'), who = [...new Set(open.map(x => x.userName))]; let p = document.getElementById('askpill');
if (open.length > askWatch.n) { try { toast('🙋 ' + who.join(', ') + (open.some(x => x.time) ? ' is asking for time' : ' is asking')); const a = new (window.AudioContext || window.webkitAudioContext)(), o = a.createOscillator(), g = a.createGain(); o.connect(g); g.connect(a.destination); o.frequency.value = 880; g.gain.setValueAtTime(.15, a.currentTime); g.gain.exponentialRampToValueAtTime(.001, a.currentTime + .5); o.start(); o.stop(a.currentTime + .5); } catch {} }
askWatch.n = open.length; document.title = (open.length ? '(' + open.length + ') ' : '') + base;
if (!open.length) { if (p) p.remove(); return; }
if (!p) { p = document.createElement('button'); p.id = 'askpill'; p.type = 'button'; p.onclick = () => { location.hash = '#/settings/family'; if (askWatch.tick) askWatch.tick(); }; document.body.appendChild(p); }
p.textContent = '🙋 ' + (who.length > 2 ? who.length + ' children are' : who.join(' and ') + (who.length > 1 ? ' are' : ' is')) + ' asking' + (open.some(x => x.time) ? ' for time' : '') + (open.length > 1 ? ' (' + open.length + ')' : '');
};
askWatch.tick = tick; await tick(); askWatch.timer = setInterval(tick, 20e3); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') tick(); });
}
/** Which part of the app the child is in, so the parents can see where the time goes (and limit one part). */
function kidArea() { const r = (location.hash.slice(2).split('/')[0] || 'home'), h = location.hash; return r === 'video' ? 'video' : r === 'music' ? 'music' : /^(audio|podcast|radio|listen|vbv)$/.test(r) || /^#\/store\/(audiobooks|radio)/.test(h) ? 'listen' : /^(read|reading|bible)$/.test(r) || /^#\/store\/books/.test(h) ? 'read' : /^(chat|signal)$/.test(r) ? 'chat' : 'other'; }
const actSeen = new Map();