Parent controls for children, name-tile sign-in, restricted Learn search, simple mode
build-container / build (push) Successful in 3m52s
build-container / build (push) Successful in 3m52s
- Sign-in page shows name tiles on remembered devices; tap a name, then PIN or passkey - Music and Movies are separate on the server; Plex can be locked to a child's managed profile - Activity log, weekly summary and alerts to parents; per-area and weekend limits - Requests from children (Ask button) answered in Settings; bonus screen time from rewards - Restricted Learn search (Simple English Wikipedia, blocked words) replaces web search for children - Simple mode (big-button home) for young children or grandparents - Child usage kept in memory instead of read from disk on every request - Tests and README updated Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aac6f6efc2
commit
301e8017d9
34 files changed
+466
-51
No files matched your search
+4
-2
@@ -42,10 +42,12 @@ async function addPersonSheet() {
|
||||
|
||||
function personSheet(u) {
|
||||
const self = u.id === ME.id, admin = u.role === 'admin';
|
||||
const el = openSheet(u.name, admin ? 'Admin' : u.disabled ? 'Turned off' : 'User', `<form id="uf"><label for="un">Name</label><input type="text" id="un" value="${esc(u.name)}" autocomplete="off" required><label class="row" style="cursor:pointer"><span class="grow"><b>Show on the family board</b><small style="white-space:normal">Their name appears on the family screen, and their chores in the family progress</small></span><input type="checkbox" id="ubd"${u.board === false ? '' : ' checked'}></label>
|
||||
const el = openSheet(u.name, admin ? 'Admin' : u.disabled ? 'Turned off' : 'User', `<form id="uf"><label for="un">Name</label><input type="text" id="un" value="${esc(u.name)}" autocomplete="off" required><label class="row" style="cursor:pointer"><span class="grow"><b>Simple mode</b><small style="white-space:normal">No menus: just a Home screen of big buttons for what they can use. Good for young children or grandparents.</small></span><input type="checkbox" id="usm"${u.simple ? ' checked' : ''}></label><label class="row" style="cursor:pointer"><span class="grow"><b>Show on the family board</b><small style="white-space:normal">Their name appears on the family screen, and their chores in the family progress</small></span><input type="checkbox" id="ubd"${u.board === false ? '' : ' checked'}></label>
|
||||
${admin ? '<p class="sub">Admins can use everything.</p>' : `<label>What can they use?</label>${tabBoxes(u.tabsChosen)}
|
||||
<h2 style="margin-top:14px">Child account</h2><label class="row" style="cursor:pointer"><span class="grow"><b>This is a child</b><small style="white-space:normal">Adds a daily time limit and a bedtime, and a safer Store and podcast search.</small></span><input type="checkbox" id="kon"${u.kid && u.kid.on ? ' checked' : ''}></label>
|
||||
<div id="kopt" ${u.kid && u.kid.on ? '' : 'hidden'}><label for="klim">Time per day, in minutes (empty for no limit)</label><input type="number" id="klim" min="0" max="720" value="${u.kid && u.kid.limitMin ? u.kid.limitMin : ''}" placeholder="120">
|
||||
<label for="kwe">Weekends, in minutes (empty: same as other days)</label><input type="number" id="kwe" min="0" max="720" value="${u.kid && u.kid.weekendMin ? u.kid.weekendMin : ''}" placeholder="180">
|
||||
<label>Limit one part of the app (minutes a day, empty for no limit)</label><div class="list" id="kal">${[['video', 'Movies and TV'], ['music', 'Music'], ['listen', 'Podcasts, audiobooks, radio'], ['read', 'Books'], ['chat', 'Chat']].map(([k, l]) => `<div class="row"><span class="grow"><b>${l}</b></span><input type="number" data-a="${k}" min="0" max="720" style="width:90px" value="${u.kid && u.kid.areaLimits && u.kid.areaLimits[k] ? u.kid.areaLimits[k] : ''}" placeholder="none"></div>`).join('')}</div>
|
||||
<label class="row" style="cursor:pointer"><span class="grow"><b>Bedtime</b><small>The app is locked in these hours</small></span><input type="checkbox" id="kbo"${u.kid && u.kid.bedtime && u.kid.bedtime.on ? ' checked' : ''}></label>
|
||||
<div class="bar" style="gap:10px;flex-wrap:nowrap"><div style="flex:1"><label for="kbf">From</label><input type="time" id="kbf" value="${esc((u.kid && u.kid.bedtime && u.kid.bedtime.from) || '20:30')}"></div><div style="flex:1"><label for="kbt">To</label><input type="time" id="kbt" value="${esc((u.kid && u.kid.bedtime && u.kid.bedtime.to) || '07:00')}"></div></div>
|
||||
<label class="row" style="cursor:pointer"><span class="grow"><b>Safer Store and podcast search</b><small style="white-space:normal">Books and audiobooks from a few kid-friendly sources and genres, and no podcasts marked explicit</small></span><input type="checkbox" id="ksf"${!u.kid || u.kid.safe !== false ? ' checked' : ''}></label>
|
||||
@@ -65,7 +67,7 @@ function personSheet(u) {
|
||||
kon.onchange = () => { el.querySelector('#kopt').hidden = !kon.checked; }; fillUse();
|
||||
el.querySelectorAll('#kuse [data-m]').forEach(b => b.onclick = async () => { try { await jfetch('/api/kid/extra', { method: 'POST', body: JSON.stringify({ user: u.id, minutes: +b.dataset.m }) }); toast('Added ' + b.dataset.m + ' minutes for today'); fillUse(); } catch (err) { toast(err.message); } });
|
||||
}
|
||||
el.querySelector('#uf').onsubmit = e => { e.preventDefault(); act(() => put({ name: el.querySelector('#un').value, board: el.querySelector('#ubd').checked, ...(admin ? {} : { tabs: pickedTabs(el), kid: { on: el.querySelector('#kon').checked, limitMin: +el.querySelector('#klim').value || 0, bedtime: { on: el.querySelector('#kbo').checked, from: el.querySelector('#kbf').value, to: el.querySelector('#kbt').value }, safe: el.querySelector('#ksf').checked } }) }), 'Saved'); };
|
||||
el.querySelector('#uf').onsubmit = e => { e.preventDefault(); act(() => put({ name: el.querySelector('#un').value, board: el.querySelector('#ubd').checked, simple: admin ? undefined : el.querySelector('#usm').checked, ...(admin ? {} : { tabs: pickedTabs(el), kid: { on: el.querySelector('#kon').checked, limitMin: +el.querySelector('#klim').value || 0, weekendMin: +el.querySelector('#kwe').value || 0, areaLimits: Object.fromEntries([...el.querySelectorAll('#kal [data-a]')].map(i => [i.dataset.a, +i.value || 0])), bedtime: { on: el.querySelector('#kbo').checked, from: el.querySelector('#kbf').value, to: el.querySelector('#kbt').value }, safe: el.querySelector('#ksf').checked } }) }), 'Saved'); };
|
||||
el.querySelector('#u-inv').onclick = async () => { try { const r = await jfetch('/api/users/' + encodeURIComponent(u.id) + '/invite', { method: 'POST', body: '{}' }); codeSheet(u.name, r.code, r.minutes); } catch (err) { toast(err.message); } };
|
||||
el.querySelector('#u-out').onclick = () => act(() => put({ signOutEverywhere: true }), u.name + ' was signed out everywhere');
|
||||
const off = el.querySelector('#u-off'); if (off) off.onclick = () => act(() => put({ disabled: !u.disabled }), u.disabled ? 'Turned back on' : 'Turned off');
|
||||
|
||||
Reference in new issue
Block a user