feat(notify): web push as a notification channel
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m15s

- VAPID keys from VAPID_* or generated once (push_keys, migration 9);
  devices in push_subscriptions, forgotten on 404/410 from the push service
- push column for every event (also due tasks) and the daily summary,
  bundles as one notification; /push/key, /devices, /subscribe,
  /unsubscribe, /test
- settings: switch push on per device, test it, see the devices; the
  service worker shows notifications and focuses MindSift on click
- tests decrypt a sent push like a browser (RFC 8291) and check VAPID

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-30 21:21:55 +01:00
1 parent 1a49e6b304
commit 38b8c92899
27 files changed
+918 -27

No files matched your search

+22
View File
@@ -5,6 +5,7 @@ import {
Document,
TrashedDocument,
JournalDay,
PushDevice,
BackupFile,
DataSection,
ApiKeyScope,
@@ -724,6 +725,27 @@ export const api = {
return request<TrashedDocument[]>('/api/v1/documents/trash');
},
async getPushKey(): Promise<{ enabled: boolean; publicKey?: string }> {
return request('/api/v1/push/key');
},
async getPushDevices(): Promise<PushDevice[]> {
const res = await request<{ devices: PushDevice[] }>('/api/v1/push/devices');
return res.devices || [];
},
async subscribePush(sub: PushSubscriptionJSON, device: string): Promise<void> {
await request('/api/v1/push/subscribe', { method: 'POST', body: JSON.stringify({ ...sub, device }) });
},
async unsubscribePush(endpoint: string): Promise<void> {
await request('/api/v1/push/unsubscribe', { method: 'POST', body: JSON.stringify({ endpoint }) });
},
async testPush(): Promise<{ devices: number }> {
return request('/api/v1/push/test', { method: 'POST' });
},
/** Days of the own journal (newest first) with their notes. */
async getJournal(): Promise<JournalDay[]> {
const res = await request<{ days: JournalDay[] }>('/api/v1/journal');