Web Push reminders for tasks and events (Settings -> Reminders)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a0f1efc4ef
commit
c1f4a220f9
10 files changed
+426
-9
No files matched your search
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
// Push reminders: who gets told, when, and never twice. The push service itself is replaced by a recorder.
|
||||
const fs = require('fs'), os = require('os'), path = require('path');
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-push-'));
|
||||
process.env.DATA_DIR = dir; process.env.COOKIE_SECRET = 'push-test-secret-'.repeat(3);
|
||||
let failed = 0;
|
||||
const ok = (n, c, x) => { console.log((c ? 'PASS ' : 'FAIL ') + n + (c ? '' : ' -> ' + JSON.stringify(x))); if (!c) failed++; };
|
||||
|
||||
const webpush = require('web-push');
|
||||
const sent = []; let dead = false;
|
||||
webpush.sendNotification = async (sub, payload) => { if (dead) throw Object.assign(new Error('gone'), { statusCode: 410 }); sent.push({ sub, ...JSON.parse(payload) }); };
|
||||
const users = require('../users'), push = require('../push');
|
||||
|
||||
(async () => {
|
||||
const u = users.create({ name: 'Pat', role: 'user', tabs: ['tasks', 'calendar'] });
|
||||
const d = users.dirOf(u.id);
|
||||
const TZ = 'America/New_York', now = new Date('2026-10-22T13:00:00Z'); // 09:00 in New York
|
||||
const { day, hm } = push.localNow(TZ, now);
|
||||
ok('the person\'s own clock is used (09:00 in New York at 13:00 UTC)', day === '2026-10-22' && hm === '09:00', { day, hm });
|
||||
const writePush = o => fs.writeFileSync(path.join(d, 'push.json'), JSON.stringify({ tz: TZ, tasksAt: '08:00', eventMins: 15, tasks: true, events: true, subs: [{ sub: { endpoint: 'https://push.example/abc', keys: { p256dh: 'x', auth: 'y' } } }], ...o }));
|
||||
fs.writeFileSync(path.join(d, 'tasks.json'), JSON.stringify({ lists: [{ id: 'inbox', name: 'Tasks' }], tasks: [
|
||||
{ id: 't1', list: 'inbox', title: 'Pay rent', due: day, done: false }, { id: 't2', list: 'inbox', title: 'Old thing', due: '2026-10-01', done: false },
|
||||
{ id: 't3', list: 'inbox', title: 'Done thing', due: day, done: true }, { id: 't4', list: 'inbox', title: 'Later', due: '2026-12-01', done: false }] }));
|
||||
|
||||
writePush({}); await push.tick(new Date('2026-10-22T11:00:00Z')); // 07:00 local: too early
|
||||
ok('nothing is sent before the morning time', sent.length === 0, sent);
|
||||
await push.tick(now);
|
||||
ok('at the morning time one summary is sent for tasks due or overdue', sent.length === 1 && /2 tasks/.test(sent[0].title) && /1 overdue/.test(sent[0].body) && !/Done thing|Later/.test(sent[0].body), sent);
|
||||
await push.tick(new Date(now.getTime() + 60e3));
|
||||
ok('and not again the same day', sent.length === 1, sent.length);
|
||||
await push.tick(new Date(now.getTime() + 864e5));
|
||||
ok('but again the next day', sent.length === 2, sent.length);
|
||||
|
||||
sent.length = 0; writePush({ tasks: false, subs: [] }); fs.writeFileSync(path.join(d, 'push.json'), JSON.stringify({ tz: TZ, subs: [] }));
|
||||
await push.tick(new Date(now.getTime() + 2 * 864e5));
|
||||
ok('a person with no device signed up is never contacted', sent.length === 0, sent);
|
||||
|
||||
// events: a local event starting in 10 minutes
|
||||
const soon = new Date(Date.now() + 10 * 60e3), pad = n => String(n).padStart(2, '0');
|
||||
const st = `${soon.getUTCFullYear()}-${pad(soon.getUTCMonth() + 1)}-${pad(soon.getUTCDate())}T${pad(soon.getUTCHours())}:${pad(soon.getUTCMinutes())}`;
|
||||
fs.writeFileSync(path.join(d, 'events.json'), JSON.stringify([{ uid: 'e1', fields: { title: 'Dentist', location: 'Main St', notes: '', allDay: false, start: st, end: st.replace(/T(\d\d)/, (m, h) => 'T' + pad((+h + 1) % 24)), tz: 'UTC', repeat: 'none' }, created: 1, updated: 1 }]));
|
||||
writePush({ tasks: false }); sent.length = 0;
|
||||
await push.tick(new Date(Date.now() - 3 * 36e5), true);
|
||||
ok('an event hours away sends nothing', sent.length === 0, sent);
|
||||
await push.tick(new Date(), true);
|
||||
ok('an event about to start is announced with its place', sent.some(s => s.title === 'Dentist' && /Main St/.test(s.body)), sent);
|
||||
await push.tick(new Date(Date.now() + 60e3), true);
|
||||
ok('and only once', sent.filter(s => s.title === 'Dentist').length === 1, sent);
|
||||
|
||||
// a subscription the push service says is gone is dropped
|
||||
dead = true; writePush({ tasks: true, tasksAt: '00:00', tasksDay: '' }); fs.writeFileSync(path.join(d, 'tasks.json'), JSON.stringify({ lists: [{ id: 'inbox', name: 'Tasks' }], tasks: [{ id: 't1', list: 'inbox', title: 'X', due: '2000-01-01', done: false }] }));
|
||||
await push.tick(new Date());
|
||||
ok('a device that has unsubscribed (410) is removed', JSON.parse(fs.readFileSync(path.join(d, 'push.json'), 'utf8')).subs.length === 0, fs.readFileSync(path.join(d, 'push.json'), 'utf8'));
|
||||
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
console.log(failed ? `\n${failed} check(s) failed` : '\nAll push checks passed'); process.exit(failed ? 1 : 0);
|
||||
})().catch(e => { console.error('TEST CRASHED:', e); process.exit(1); });
|
||||
Reference in new issue
Block a user