Files
hub/public/settings-family.js
T
nmemmertandClaude Sonnet 5.5 b1c720c74f
build-container / build (push) Successful in 4m9s
Time requests from the lock screen: one request with +15/+30/+1 h buttons, lock lifts quickly
- Ask a parent on the lock screen also makes a request parents answer in Settings > Family
- Parents choose how many minutes; the child's lock screen rechecks every few seconds
- Notification links open Settings > Family
- Test updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 17:03:43 -04:00

60 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
// ---- Settings > Family: the family's settings in one place (chat, your PIN, chores and rewards, who is on the family board, shared screens).
// Each row opens the same sheet that used to live inside that part of the hub.
const famRow = (id, ic, title, sub) => `<button class="row" id="${id}"><span class="ic" style="background:#6a7280">${icon(ic)}</span><span class="grow"><b>${esc(title)}</b><small style="white-space:normal">${esc(sub)}</small></span>${icon('chev', 'chev')}</button>`;
async function famManageChores() {
try { cr.data = await crApi('/?date=' + crYmd(new Date())); } catch (e) { return toast(e.message); }
cr.afterEdit = famManageChores; crManage();
}
async function famSettings(box) {
if (!box) return; const admin = ME && ME.role === 'admin', can = t => typeof allowed === 'function' && allowed(t);
box.innerHTML = `<h2>You</h2><div class="list">${can('chat') ? famRow('fs-chat', 'chat', 'Chat', 'Quiet hours for phone alerts' + (admin ? ', and the family announcement' : '')) : ''}</div><div id="fs-dev"></div>
${admin && can('chores') ? `<h2>Chores</h2><div class="list">${famRow('fs-chores', 'check', 'Chores and weekly rewards', 'Make chores, choose who does them, set the points and the reward tiers')}</div>` : ''}
${admin ? '<h2>Children</h2><div id="fs-req"></div><p class="sub" style="margin:8px 0">What each child did this week, and Plex.</p><div class="list" id="fs-kids"><div class="row"><span class="grow"><small>Loading…</small></span></div></div><div class="list" style="margin-top:10px">' + famRow('fs-words', 'search', 'Blocked search words', 'A child\'s search only uses Simple English Wikipedia and a built-in list of blocked words. Add your own words here.') + '</div>' : ''}
${admin ? '<h2>Family board</h2><p class="sub" style="margin:0 0 8px">Who has a name tile on the shared family screen and shows in its chore progress.</p><div class="list" id="fs-board"><div class="row"><span class="grow"><small>Loading…</small></span></div></div>' : ''}`;
const c = box.querySelector('#fs-chat'); if (c) c.onclick = () => chSettings();
const k = box.querySelector('#fs-chores'); if (k) k.onclick = famManageChores;
if (typeof devSettings === 'function') devSettings(box.querySelector('#fs-dev'));
if (admin) { famRequests(box.querySelector('#fs-req')); famKids(box.querySelector('#fs-kids')); const w = box.querySelector('#fs-words'); if (w) w.onclick = famWords; }
if (admin) try {
const r = await jfetch('/api/users'), b = box.querySelector('#fs-board'); if (!b) return;
b.innerHTML = r.users.filter(u => !u.disabled).map(u => `<label class="row" style="cursor:pointer"><span class="ic" style="background:${u.role === 'admin' ? '#4f7cff' : '#6a7280'}">${esc(u.name.slice(0, 1).toUpperCase())}</span><span class="grow"><b>${esc(u.name)}</b>${u.kid ? '<small>Child</small>' : ''}</span><input type="checkbox" data-u="${esc(u.id)}" ${u.board === false ? '' : 'checked'}></label>`).join('');
b.querySelectorAll('input[data-u]').forEach(i => i.onchange = async () => { try { await jfetch('/api/users/' + encodeURIComponent(i.dataset.u), { method: 'PUT', body: JSON.stringify({ board: i.checked }) }); } catch (e) { toast(e.message); i.checked = !i.checked; } });
} catch (e) { const b = box.querySelector('#fs-board'); if (b) b.innerHTML = `<div class="row"><span class="grow"><small>${esc(e.message)}</small></span></div>`; }
}
// ---- Requests from children: what they asked for, with Yes / No.
async function famRequests(box) {
if (!box) return; let r; try { r = await jfetch('/api/kid/requests'); } catch { box.innerHTML = ''; return; }
const open = r.requests.filter(x => x.status === 'open'); if (!open.length) { box.innerHTML = ''; return; }
const when = t => new Date(t).toLocaleString([], { weekday: 'short', hour: 'numeric', minute: '2-digit' });
box.innerHTML = `<h2 style="margin-top:0">Asking you (${open.length})</h2><div class="list">${open.map(q => `<div class="row" style="flex-wrap:wrap"><span class="grow" style="min-width:200px"><b>${esc(q.userName)}: ${esc(q.text)}</b><small style="white-space:normal">${when(q.at)}${q.tab ? ' · wants ' + esc((TABS.find(t => t[0] === q.tab) || [0, q.tab])[1]) : ''}${q.minutes ? ' · +' + q.minutes + ' min' : ''}</small></span>${q.time ? [15, 30, 60].map(m => `<button class="btn" data-q="${esc(q.id)}" data-y="1" data-m="${m}">+${m === 60 ? '1 h' : m + ' min'}</button>`).join('') : `<button class="btn" data-q="${esc(q.id)}" data-y="1">Yes</button>`}<button class="btn ghost" data-q="${esc(q.id)}">No</button></div>`).join('')}</div>`;
box.querySelectorAll('[data-q]').forEach(b => b.onclick = async () => { const yes = !!b.dataset.y; try { await jfetch('/api/kid/requests/' + b.dataset.q, { method: 'POST', body: JSON.stringify({ approve: yes, minutes: b.dataset.m ? +b.dataset.m : undefined }) }); toast(yes ? 'Said yes' : 'Said no'); famRequests(box); } catch (e) { toast(e.message); } });
}
// ---- One row per child: their week (minutes by area, what they played, searched and tried), and Plex locking.
async function famKids(box) {
if (!box) return; let r; try { r = await jfetch('/api/users'); } catch (e) { box.innerHTML = `<div class="row"><span class="grow"><small>${esc(e.message)}</small></span></div>`; return; }
const kids = r.users.filter(u => u.kid && u.kid.on && !u.disabled);
box.innerHTML = kids.map(u => `<button class="row" data-k="${esc(u.id)}"><span class="ic" style="background:#2f9e6e">${esc(u.name.slice(0, 1).toUpperCase())}</span><span class="grow"><b>${esc(u.name)}</b><small>Activity and Plex</small></span>${icon('chev', 'chev')}</button>`).join('') || '<div class="row"><span class="grow"><small>No child accounts yet. Turn on <b>This is a child</b> for a person under Admin → People.</small></span></div>';
box.querySelectorAll('[data-k]').forEach(b => b.onclick = () => famChild(kids.find(u => u.id === b.dataset.k)));
}
async function famChild(u) {
const el = openSheet(u.name, 'Last 7 days', '<div id="fcb"><p class="empty">Loading…</p></div>'); let a, px;
try { [a, px] = await Promise.all([jfetch('/api/kid/activity?user=' + encodeURIComponent(u.id) + '&days=7'), jfetch('/api/kid/plex-lock?user=' + encodeURIComponent(u.id))]); } catch (e) { el.querySelector('#fcb').innerHTML = `<p class="err">${esc(e.message)}</p>`; return; }
const h = m => m >= 60 ? Math.floor(m / 60) + ' h ' + (m % 60) + ' min' : m + ' min', total = a.days.reduce((n, d) => n + d.minutes, 0), by = {}; a.days.forEach(d => Object.entries(d.areas).forEach(([k, m]) => { by[k] = (by[k] || 0) + m; }));
const icn = { play: '🎵', watch: '🎬', listen: '🎧', read: '📖', search: '🔎', signin: '🔑', chore: '✅', add: '➕', blocked: '⛔' }, max = Math.max(1, ...a.days.map(d => d.minutes));
const day = d => new Date(d + 'T12:00:00').toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
el.querySelector('#fcb').innerHTML = `<div class="list"><div class="row"><span class="grow"><b>${h(total)} in the app</b><small style="white-space:normal">${Object.entries(by).filter(([k, m]) => m > 0 && a.labels[k]).map(([k, m]) => esc(a.labels[k]) + ' ' + h(m)).join(' · ') || 'No time recorded yet'}</small></span></div>
${a.days.map(d => `<div class="row"><span class="grow"><small>${esc(day(d.day))}</small><div class="statbar"><i style="width:${Math.round(d.minutes / max * 100)}%"></i></div></span><small>${h(d.minutes)}</small></div>`).join('')}</div>
<h2 style="margin:14px 0 6px">Plex</h2><div class="list"><div class="row"><span class="grow"><b>${px.connected ? (px.profile ? 'Using Plex as ' + esc(px.profile) : 'Connected to Plex') : 'Not connected to Plex'}</b><small style="white-space:normal">${px.locked ? 'Locked to this profile: they cannot switch profiles or servers.' : 'They can switch Plex profiles if the other profile has no PIN. Lock it once you are on their managed profile.'}</small></span><button class="btn ${px.locked ? 'ghost' : ''}" id="fcpl">${px.locked ? 'Unlock' : 'Lock'}</button></div></div>
<h2 style="margin:14px 0 6px">What they did</h2><div class="list" style="max-height:38dvh;overflow:auto">${a.recent.map(e => `<div class="row"><span style="font-size:20px">${icn[e.kind] || '•'}</span><span class="grow"><b style="white-space:normal">${esc(e.title || e.kind)}</b><small>${esc(new Date(e.t).toLocaleString([], { weekday: 'short', hour: 'numeric', minute: '2-digit' }))}</small></span></div>`).join('') || '<p class="empty" style="margin:8px">Nothing yet.</p>'}</div>`;
el.querySelector('#fcpl').onclick = async () => { try { await jfetch('/api/kid/plex-lock', { method: 'POST', body: JSON.stringify({ user: u.id, locked: !px.locked }) }); toast(px.locked ? 'Unlocked' : 'Locked to this profile'); closeSheet(); famChild(u); } catch (e) { toast(e.message); } };
}
// ---- The words a parent blocks from a child's search (on top of a built-in list).
async function famWords() {
let r; try { r = await jfetch('/api/kidsearch/words'); } catch (e) { return toast(e.message); }
const el = openSheet('Blocked search words', 'A child searches Simple English Wikipedia only. Any search or result with one of these words (or a built-in blocked word) is hidden. One per line or separated by commas.', `<form id="fwf"><textarea id="fww" rows="6" style="width:100%" placeholder="a word you want blocked">${esc(r.words.join('\n'))}</textarea><div class="bar"><button type="button" class="btn ghost" id="fwx" style="flex:1;justify-content:center">Cancel</button><button class="btn" style="flex:1;justify-content:center">Save</button></div></form>`);
el.querySelector('#fwx').onclick = closeSheet; el.querySelector('#fwf').onsubmit = async e => { e.preventDefault(); try { await jfetch('/api/kidsearch/words', { method: 'POST', body: JSON.stringify({ words: el.querySelector('#fww').value }) }); closeSheet(); toast('Saved'); } catch (x) { toast(x.message); } };
}