Changeset 053bf51 in Klonkt


Ignore:
Timestamp:
07/22/2026 07:12:11 AM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
8240e80
Parents:
ad10715
git-author:
Robin <roboburr@…> (07/22/2026 07:05:42 AM)
git-committer:
Robin <roboburr@…> (07/22/2026 07:12:11 AM)
Message:

Feature: web push slice 2, enable/disable UI + service worker delivery

The visible half of docs/webpush-design.md: an owner can now turn on
notifications per device, pick what to be notified about, and send a test.

  • Routes: GET /push/vapid (the public key, public by design), POST /push/subscribe|/unsubscribe|/alerts|/test (logged-in; a subscription row is personal, only its creator may touch it). Mounted before the /:slug catch-all, like /paid.
  • Service worker: push handler (shows the encrypted JSON payload {type,title,body,url}; same-type bursts collapse via tag) and notificationclick (focus an open tab and navigate, else open a window). Cache name bumped to v19.
  • Beheer -> Notificaties (/admin/push): per-device toggle, alert-type checkboxes (saved prefs shown for the current device), test button, linked-devices list with remove, iOS install hint (push needs an installed PWA there), plain <script> so injectCspNonce provides the real nonce.
  • Not premium-gated: notifications are infrastructure, not an extra.

Verified live on a dev server: /push/vapid serves the generated key,
storage/.vapid is 0600, sw.js carries both handlers, and an unauthenticated
subscribe is refused.

Changed files:
src/server.js

  • mount /push + /admin/push; sw.js push/notificationclick handlers, v19

src/views/pages/admin.ejs

  • "Notificaties" button (always visible, not premium)

New file:
src/routes/push.js

  • vapid/subscribe/unsubscribe/alerts/test

src/routes/admin-push.js

  • the Beheer page (requireSiteManager)

src/views/pages/admin-push.ejs

  • device toggle, prefs, test, device list, iOS hint

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

Location:
src
Files:
3 added
2 edited

Legend:

Unmodified
Added
Removed
  • src/server.js

    rad10715 r053bf51  
    4747import adminStatsRoutes from './routes/admin-stats.js';
    4848import adminPaidRoutes from './routes/admin-paid.js';
     49import adminPushRoutes from './routes/admin-push.js';
     50import pushRoutes from './routes/push.js';
    4951import adminMediaRoutes from './routes/admin-media.js';
    5052import circleRoutes from './routes/circle.js';
     
    381383app.use('/admin/stats', adminStatsRoutes);
    382384app.use('/admin/paid', adminPaidRoutes);
     385app.use('/admin/push', adminPushRoutes);
    383386app.use('/admin/newsletter', adminNewsletterRoutes);
    384387app.use('/admin/shows', adminShowsRoutes);
     
    402405app.use('/', langRoutes); // /lang/:code — interface-taal kiezen (vóór de catch-all)
    403406app.use('/paid', paidRoutes);   // paid-posts patron/passkey flow (before the /:slug catch-all)
     407app.use('/push', pushRoutes);   // web-push subscribe/test (before the /:slug catch-all)
    404408app.use('/', postsRoutes);
    405409
     
    478482  res.set('Cache-Control', 'no-cache');
    479483  res.send(`
    480 const CACHE_VERSION = 'pcms-v18-' + new Date().toISOString().split('T')[0];
     484const CACHE_VERSION = 'pcms-v19-' + new Date().toISOString().split('T')[0];
    481485self.addEventListener('install', e => {
    482486  e.waitUntil(caches.open(CACHE_VERSION).then(c => c.addAll(['/'])));
     
    522526  );
    523527});
     528// Web push (docs/webpush-design.md): payload is JSON {type,title,body,url},
     529// encrypted end-to-end to this browser (RFC 8291). Show it; click opens url.
     530self.addEventListener('push', e => {
     531  let d = {};
     532  try { d = e.data ? e.data.json() : {}; } catch (err) { /* non-JSON push */ }
     533  const title = d.title || 'Klonkt';
     534  e.waitUntil(self.registration.showNotification(title, {
     535    body: d.body || '',
     536    icon: '/favicon.svg',
     537    badge: '/favicon.svg',
     538    tag: d.type ? ('klonkt-' + d.type) : undefined,   // collapse same-type bursts
     539    data: { url: d.url || '/' },
     540  }));
     541});
     542self.addEventListener('notificationclick', e => {
     543  e.notification.close();
     544  const url = (e.notification.data && e.notification.data.url) || '/';
     545  e.waitUntil(clients.matchAll({ type: 'window', includeUncontrolled: true }).then(list => {
     546    for (const c of list) {
     547      if (new URL(c.url).origin === self.location.origin && 'focus' in c) { c.navigate(url); return c.focus(); }
     548    }
     549    return clients.openWindow(url);
     550  }));
     551});
    524552  `);
    525553});
  • src/views/pages/admin.ejs

    rad10715 r053bf51  
    5555    <%# Fediverse pages (News / Notifications / My replies / Blocking) are reached via the
    5656        "Fediverse" nav button + the bell + the tabbed section — not duplicated here. %>
     57    <a href="/admin/push" class="btn">Notificaties</a>
    5758    <a href="/admin/updates" class="btn"><%= t('admin.b_updates') %></a>
    5859    <a href="/admin/handleiding" class="btn"><%= t('admin.b_help') %></a>
Note: See TracChangeset for help on using the changeset viewer.