Chores, Family page, genre search, child accounts, wider Search and Notifications center
build-container / build (push) Successful in 4m6s
build-container / build (push) Successful in 4m6s
- Chores tab with repeats, points and a week/month board - Family page: calendar, chores, shopping, notes, chat and tasks on one start page - Genre search in the Store, Free Books and LibriVox - Child accounts: safe content, daily limit and bedtime, ask a parent - Search now covers notes, chat, contacts, chores, recipes and your books - Notifications center: bell with a per-person list of chat, reminders and announcements - Roadmap, README and tests updated Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cf292e3bc7
commit
0383da87f5
38 files changed
+720
-79
No files matched your search
@@ -9,6 +9,7 @@ const users = require('./users');
|
||||
const tasks = require('./tasks');
|
||||
const cal = require('./cal');
|
||||
const mail = require('./mail');
|
||||
const inbox = require('./inbox');
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data');
|
||||
const readBody = req => new Promise((ok, no) => { const c = []; let n = 0; req.on('data', d => { n += d.length; if (n > 20000) { no(new Error('Too large')); req.destroy(); } else c.push(d); }); req.on('end', () => ok(Buffer.concat(c).toString())); req.on('error', no); });
|
||||
@@ -34,7 +35,8 @@ function localNow(tz, at = new Date()) {
|
||||
return { day: `${p.year}-${p.month}-${p.day}`, hm: `${p.hour}:${p.minute}` };
|
||||
}
|
||||
|
||||
async function send(dir, st, payload) {
|
||||
async function send(dir, st, uid, payload) {
|
||||
if (uid) inbox.add(uid, { kind: 'remind', title: payload.title, body: payload.body, url: payload.url, tag: payload.tag }); // (reminders also land in the Notifications list)
|
||||
keys();
|
||||
let changed = false;
|
||||
await Promise.all(st.subs.map(async s => {
|
||||
@@ -61,7 +63,7 @@ async function tick(now = new Date(), force = false) {
|
||||
const due = (await tasks.allTasks()).filter(t => !t.done && t.due && t.due <= lt.day);
|
||||
if (due.length) {
|
||||
const over = due.filter(t => t.due < lt.day).length;
|
||||
await send(dir, st, { title: due.length === 1 ? due[0].title : due.length + ' tasks need attention', body: due.length === 1 ? (over ? 'Overdue' : 'Due today') : due.slice(0, 3).map(t => t.title).join(', ') + (due.length > 3 ? '…' : '') + (over ? ` (${over} overdue)` : ''), url: '#/tasks', tag: 'tasks-' + lt.day });
|
||||
await send(dir, st, u.id, { title: due.length === 1 ? due[0].title : due.length + ' tasks need attention', body: due.length === 1 ? (over ? 'Overdue' : 'Due today') : due.slice(0, 3).map(t => t.title).join(', ') + (due.length > 3 ? '…' : '') + (over ? ` (${over} overdue)` : ''), url: '#/tasks', tag: 'tasks-' + lt.day });
|
||||
}
|
||||
} catch (e) { console.warn('[push] tasks check failed:', e.message); }
|
||||
}
|
||||
@@ -71,7 +73,7 @@ async function tick(now = new Date(), force = false) {
|
||||
const bucket = ss.days <= 0 ? 0 : [1, 3, 7, 14, 30].find(t => ss.days <= t), key = `mailsecret:${ss.expires}:${bucket}`;
|
||||
if (!st.sent.includes(key)) {
|
||||
st.sent.push(key); dirty = true;
|
||||
await send(dir, st, { title: ss.days <= 0 ? 'Mail sign-in has expired' : `Mail sign-in expires in ${ss.days} day${ss.days === 1 ? '' : 's'}`, body: 'Make a new client secret in Microsoft Entra and paste it in Settings -> Microsoft app.', url: '#/settings', tag: 'mailsecret' });
|
||||
await send(dir, st, u.id, { title: ss.days <= 0 ? 'Mail sign-in has expired' : `Mail sign-in expires in ${ss.days} day${ss.days === 1 ? '' : 's'}`, body: 'Make a new client secret in Microsoft Entra and paste it in Settings -> Microsoft app.', url: '#/settings', tag: 'mailsecret' });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +85,7 @@ async function tick(now = new Date(), force = false) {
|
||||
const fresh = now2.filter(m => !seen.includes(m.id));
|
||||
if (fresh.length) {
|
||||
st.mailSeen = seen.concat(fresh.map(m => m.id)).slice(-200); dirty = true;
|
||||
await send(dir, st, { title: fresh.length === 1 ? (fresh[0].from || 'New mail') : fresh.length + ' new messages', body: fresh.length === 1 ? fresh[0].subject : fresh.slice(0, 3).map(m => m.from || m.subject).join(', '), url: '#/mail', tag: 'mail' });
|
||||
await send(dir, st, u.id, { title: fresh.length === 1 ? (fresh[0].from || 'New mail') : fresh.length + ' new messages', body: fresh.length === 1 ? fresh[0].subject : fresh.slice(0, 3).map(m => m.from || m.subject).join(', '), url: '#/mail', tag: 'mail' });
|
||||
}
|
||||
}
|
||||
} catch (e) { console.warn('[push] mail check failed:', e.message); }
|
||||
@@ -99,7 +101,7 @@ async function tick(now = new Date(), force = false) {
|
||||
if (st.sent.includes(key)) continue;
|
||||
st.sent.push(key); dirty = true;
|
||||
const mins = Math.max(1, Math.round((e.start - from) / 6e4));
|
||||
await send(dir, st, { title: e.title, body: `Starts in ${mins} min${e.location ? ' · ' + e.location : ''}`, url: '#/calendar', tag: key });
|
||||
await send(dir, st, u.id, { title: e.title, body: `Starts in ${mins} min${e.location ? ' · ' + e.location : ''}`, url: '#/calendar', tag: key });
|
||||
}
|
||||
if (st.sent.length > 300) { st.sent = st.sent.slice(-200); dirty = true; }
|
||||
} catch (e) { console.warn('[push] events check failed:', e.message); }
|
||||
@@ -137,11 +139,11 @@ async function handle(req, res) {
|
||||
}
|
||||
if (p === '/api/push/test') {
|
||||
if (!st.subs.length) return json(res, 400, { error: 'No device is signed up for reminders yet' });
|
||||
await send(dir, st, { title: 'NE Hub reminders are on', body: 'You will see task and event reminders here.', url: '#/home', tag: 'test' });
|
||||
await send(dir, st, null, { title: 'NE Hub reminders are on', body: 'You will see task and event reminders here.', url: '#/home', tag: 'test' });
|
||||
return json(res, 200, { ok: true });
|
||||
}
|
||||
json(res, 404, { error: 'Not found' });
|
||||
}
|
||||
/** A notification for one person's devices (a chat message, say), unless they turned that kind off. */
|
||||
async function notify(uid, payload) { const dir = users.dirOf(uid), st = load(dir); if (!st.subs.length || st.chat === false) return; await send(dir, st, payload); }
|
||||
async function notify(uid, payload) { const dir = users.dirOf(uid), st = load(dir); if (!st.subs.length || st.chat === false) return; await send(dir, st, null, payload); }
|
||||
module.exports = { handle, start, tick, localNow, notify };
|
||||
Reference in new issue
Block a user