Parent controls for children, name-tile sign-in, restricted Learn search, simple mode
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:
nmemmertandClaude Sonnet 5.5 committed 2026-10-26 16:49:48 -04:00
1 parent aac6f6efc2
commit 301e8017d9
34 files changed
+466 -51

No files matched your search

+9
View File
@@ -38,6 +38,15 @@ const hub = http.createServer((req, res) => req.url.startsWith('/api/chores') ?
assert.equal((await call('/api/device', {}, session(ann) + '; ' + dev)).json.idleMin, 5, 'five idle minutes unless changed'); assert.equal((await post('/api/device/idle', { minutes: 2 }, session(bob))).status, 403); assert.equal((await post('/api/device/idle', { minutes: 7 }, session(ann))).status, 400, 'only the offered choices'); assert.equal((await post('/api/device/idle', { minutes: 2 }, session(ann))).status, 200); assert.equal((await call('/api/device', {}, session(ann) + '; ' + dev)).json.idleMin, 2, 'the new idle time sticks');
assert.equal((await call('/api/device', {}, session(ann) + '; ' + dev)).json.devices.length, 1);
r = await post('/api/device/remove', {}, session(ann) + '; ' + dev); assert.equal(r.status, 200); assert.equal((await call('/api/board', {}, dev)).status, 403, 'switched off: the old cookie no longer works');
// a person's own remembered device: name tiles and PIN sign-in, but not the family board or short sessions
const cy = users.create({ name: 'Cy', role: 'user', tabs: ['chat'], handle: 'Yy' }); board.setPin(cy.id, '135790');
assert.equal((await call('/api/login/people')).status, 403, 'a stranger gets no names');
r = await post('/api/device/trust', {}, session(ann)); assert.equal(r.status, 200); const own = r.set.find(c => c.startsWith('hub_device=')).split(';')[0]; assert.ok(!r.set.some(c => c.startsWith('hub_session=;')), 'remembering a device does not sign you out');
r = await call('/api/login/people', {}, own); assert.equal(r.status, 200); assert.ok(r.json.people.find(p => p.name === 'Cy').pinLen === 6 && r.json.people.find(p => p.name === 'Kai').kid, 'tiles show names, kids and PIN length');
assert.equal((await call('/api/board', {}, own)).status, 403, 'a remembered personal device is not a family board');
assert.equal((await post('/api/login/pin', { uid: cy.id, pin: '000000' }, own)).status, 401); r = await post('/api/login/pin', { uid: cy.id, pin: '135790' }, own); assert.equal(r.status, 200); const full = r.set.find(c => c.startsWith('hub_session=')); assert.ok(!/Max-Age=1800/.test(full) && /Max-Age=\d{7}/.test(full), 'a normal long session on your own device');
assert.equal((await post('/api/login/pin', { uid: kid.id }, own)).status, 200, 'a child taps their name'); assert.equal((await post('/api/login/pin', { uid: bob.id }, own)).status === 200, false, 'an adult with no valid PIN cannot'); assert.equal((await post('/api/login/pin', { uid: cy.id, pin: '135790' }, '')).status, 403, 'and only from a remembered device');
assert.equal((await post('/api/device/remove', {}, session(cy) + '; ' + own)).status, 403, 'someone else cannot forget Ann\'s device'); assert.equal((await post('/api/device/remove', {}, session(ann) + '; ' + own)).status, 200); assert.equal((await call('/api/login/people', {}, own)).status, 403, 'forgotten: the tiles are gone');
console.log('board ok');
} catch (e) { console.error(e); process.exitCode = 1; }
hub.close(); setTimeout(() => process.exit(process.exitCode || 0), 50);