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
build-container / build (push) Successful in 4m3s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d0e4e82e0a
commit
0a33119807
11 files changed
+59
-19
No files matched your search
+5
-5
@@ -33,7 +33,7 @@ function status(u) {
|
||||
const now = push.localNow(tzOf(u.id)), day = load()[u.id] && load()[u.id][now.day] || { secs: 0, extra: 0 }, base = weekend(now.day) && k.weekendMin ? k.weekendMin : k.limitMin, limit = base ? base + (day.extra || 0) : 0, used = Math.floor((day.secs || 0) / 60);
|
||||
const areas = {}, lockedAreas = []; for (const a of AREAS) { const lim = (k.areaLimits || {})[a]; if (!lim) continue; const um = Math.floor(((day.areas || {})[a] || 0) / 60); areas[a] = { used: um, limit: lim, left: Math.max(0, lim - um) }; if (um >= lim) lockedAreas.push(a); }
|
||||
const q = k.bedtime || {}, asleep = !!q.on && (q.from <= q.to ? now.hm >= q.from && now.hm < q.to : now.hm >= q.from || now.hm < q.to);
|
||||
const locked = asleep ? 'bedtime' : limit && used >= limit ? 'time' : null;
|
||||
const locked = asleep && !Object.values(load()[u.id] || {}).some(x => x && x.bedUntil > Date.now()) ? 'bedtime' : limit && used >= limit ? 'time' : null;
|
||||
return { on: true, used, limit, left: limit ? Math.max(0, limit - used) : null, areas, lockedAreas, extra: day.extra || 0, bedtime: q.on ? { from: q.from, to: q.to } : null, locked, day: now.day };
|
||||
}
|
||||
function ping(u, area) {
|
||||
@@ -47,7 +47,7 @@ function ping(u, area) {
|
||||
/** Minutes per day (newest first) with the split by area, for the parents' view. */
|
||||
function usageByDay(uid, days = 7) { const m = load()[uid] || {}; return Object.keys(m).sort().reverse().slice(0, days).map(day => ({ day, minutes: Math.floor((m[day].secs || 0) / 60), extra: m[day].extra || 0, areas: Object.fromEntries(Object.entries(m[day].areas || {}).map(([a, s]) => [a, Math.floor(s / 60)])) })); }
|
||||
const st0 = u => status(u);
|
||||
function give(uid, minutes) { const u = users.get(uid); if (!u || !u.kid || !u.kid.on) throw fail(400, 'That is not a child account'); const d = load(), now = push.localNow(tzOf(uid)), mine = d[uid] || (d[uid] = {}), day = mine[now.day] || (mine[now.day] = { secs: 0, extra: 0, last: 0 }); day.extra = Math.max(0, Math.min(600, (day.extra || 0) + Math.round(+minutes || 0))); save(d); return status(u); }
|
||||
function give(uid, minutes, liftBedtime) { const u = users.get(uid); if (!u || !u.kid || !u.kid.on) throw fail(400, 'That is not a child account'); const d = load(), now = push.localNow(tzOf(uid)), mine = d[uid] || (d[uid] = {}), day = mine[now.day] || (mine[now.day] = { secs: 0, extra: 0, last: 0 }); const wasBed = liftBedtime && +minutes > 0 && status(u).locked === 'bedtime'; day.extra = Math.max(0, Math.min(600, (day.extra || 0) + Math.round(+minutes || 0))); if (wasBed) day.bedUntil = Date.now() + Math.round(+minutes) * 60e3; /* (a yes at bedtime opens the app for that long) */ save(d); return status(u); }
|
||||
const ALLOWED_WHILE_LOCKED = /^\/api\/(me|kid(\/|$)|csp-report|push\/)/;
|
||||
/** For the server: should this request be refused because the account is locked? Returns the status when it should. */
|
||||
function lockedFor(u, pathname) {
|
||||
@@ -59,14 +59,14 @@ async function handle(req, res, me, hooks = {}) {
|
||||
const u = new URL(req.url, 'http://x'), p = u.pathname.slice('/api/kid'.length) || '/';
|
||||
try {
|
||||
if (p === '/status' && req.method === 'GET') return json(res, 200, status(me));
|
||||
if (p === '/ping' && req.method === 'POST') { const b = JSON.parse(await readBody(req, 2e3) || '{}'); return json(res, 200, ping(me, String(b.area || ''))); }
|
||||
if (p === '/ping' && req.method === 'POST') { const b = JSON.parse(await readBody(req, 2e3) || '{}'); if (b.tz) push.setTz(me.id, String(b.tz)); /* (bedtime follows the child's own clock) */ return json(res, 200, ping(me, String(b.area || ''))); }
|
||||
if (p === '/activity' && req.method === 'POST') { const b = JSON.parse(await readBody(req, 4e3) || '{}'); activity.log(me.id, String(b.kind || ''), b.title); return json(res, 200, { ok: true }); } // (what a child played or read, reported by the app)
|
||||
if (p === '/activity' && req.method === 'GET') { // the parents' view of one child
|
||||
if (me.role !== 'admin') throw fail(403, 'Only an admin can see this'); const t = users.get(u.searchParams.get('user') || ''); if (!t || !activity.isKid(t)) throw fail(404, 'Child not found'); const days = Math.max(1, Math.min(30, +u.searchParams.get('days') || 7));
|
||||
return json(res, 200, { name: t.name, days: usageByDay(t.id, days), recent: activity.recent(t.id, days).slice(0, 200), labels: AREA_LABEL });
|
||||
}
|
||||
if (p === '/ask' && req.method === 'POST') { if (!me.kid || !me.kid.on) throw fail(400, 'This is for child accounts'); const b = JSON.parse(await readBody(req, 1e4) || '{}'), text = String(b.text || '').slice(0, 300).trim() || 'Can I have a little more time, please?'; const n = hooks.askParents ? hooks.askParents(me, text) : 0; require('./requests').askTime(me, text, st0(me).locked === 'bedtime' ? 15 : 30); return json(res, 200, { ok: true, told: n }); }
|
||||
if (p === '/extra' && req.method === 'POST') { if (me.role !== 'admin') throw fail(403, 'Only a parent (an admin) can give more time'); const b = JSON.parse(await readBody(req, 1e4) || '{}'); return json(res, 200, give(String(b.user || ''), b.minutes)); }
|
||||
if (p === '/ask' && req.method === 'POST') { if (!me.kid || !me.kid.on) throw fail(400, 'This is for child accounts'); const b = JSON.parse(await readBody(req, 1e4) || '{}'), text = String(b.text || '').slice(0, 300).trim() || 'Can I have a little more time, please?'; const n = users.all().filter(x => x.role === 'admin' && !x.disabled && x.id !== me.id).length; /* (parents get one urgent notification from the request itself, not a second one from a chat message) */ require('./requests').askTime(me, text, st0(me).locked === 'bedtime' ? 15 : 30); return json(res, 200, { ok: true, told: n }); }
|
||||
if (p === '/extra' && req.method === 'POST') { if (me.role !== 'admin') throw fail(403, 'Only a parent (an admin) can give more time'); const b = JSON.parse(await readBody(req, 1e4) || '{}'); return json(res, 200, give(String(b.user || ''), b.minutes, true)); }
|
||||
if (p === '/usage' && req.method === 'GET') { if (me.role !== 'admin') throw fail(403, 'Only an admin can see this'); const t = users.get(u.searchParams.get('user') || ''); if (!t) throw fail(404, 'Person not found'); return json(res, 200, status(t)); }
|
||||
return json(res, 404, { error: 'Not found' });
|
||||
} catch (e) { return json(res, e.code >= 400 && e.code < 600 ? e.code : 400, { error: e instanceof SyntaxError ? 'Bad request' : e.message }); }
|
||||
|
||||
Reference in new issue
Block a user