Index: src/assets/js/guardian.js
===================================================================
--- src/assets/js/guardian.js	(revision 060d4a512c8521a8df44895c3c233dee3ed00795)
+++ src/assets/js/guardian.js	(revision 6fab3f3e28add90b1140273d9a28793236cd7a39)
@@ -967,4 +967,35 @@
   }
 
+  // ── De lange poll (Barts opdracht, 9-8) ──────────────────────────────
+  //
+  // Het paneel tikte elke 45 seconden, ongeacht of er iets gebeurd was. Nu hangt
+  // er een verzoek open tot er iets is dat je moet verwerken -- een aanbod, een
+  // volgverzoek, een gate-voorstel, een hulpvraag. Dat is niet alleen zuiniger,
+  // het is ook SNELLER: een hulproep stond eerst tot drie kwartier van een
+  // minuut op het scherm te wachten.
+  //
+  // 304 = stilte, en dan meteen opnieuw wachten. Alleen bij 200 wordt er iets
+  // opnieuw getekend.
+  var pollBezig = false;
+  function longPoll() {
+    if (pollBezig || document.hidden) return;
+    pollBezig = true;
+    fetch('/guardian/api/state?wait=25&site=' + encodeURIComponent(S.site))
+      .then(function (r) {
+        pollBezig = false;
+        if (r.status === 304) return null;          // stilte
+        return r.json().then(function (s) {
+          if (s && !s.error) { S = s; T = s.strings || T; renderAll(); }
+          return loadFeed().then(loadFollowReqs);
+        });
+      })
+      .then(function () { setTimeout(longPoll, 200); })
+      .catch(function () {
+        // Netwerk weg of server herstart: niet meteen opnieuw beuken.
+        pollBezig = false;
+        setTimeout(longPoll, 5000);
+      });
+  }
+
   // ── 2. Adopt ───────────────────────────────────────────────────────────
   var form = document.getElementById('adopt-form');
@@ -1072,6 +1103,15 @@
   // kijkt: dat waren verzoeken voor een tabblad op de achtergrond. Bij terugkomen
   // meteen een keer, want dan is de kans op nieuws het grootst.
-  setInterval(function () { if (!document.hidden) refresh(); }, 45000);
-  document.addEventListener('visibilitychange', function () { if (!document.hidden) refresh(); });
+  // De lange poll doet het werk. Het tikje blijft als vangnet, maar veel trager:
+  // valt er een wekker weg (een pad dat niet wekt, een herstart midden in een
+  // verzoek), dan mag het scherm niet voorgoed stilstaan. Vijf minuten in plaats
+  // van vijfenveertig seconden, en met de ETag kost zo'n tik meestal een lege 304.
+  longPoll();
+  setInterval(function () { if (!document.hidden) refresh(); }, 300000);
+  document.addEventListener('visibilitychange', function () {
+    if (document.hidden) return;
+    refresh();
+    longPoll();       // bij terugkomst weer een wachter openzetten
+  });
   } catch (e) {
     fatal((e && e.message) || String(e));
Index: src/routes/guardian.js
===================================================================
--- src/routes/guardian.js	(revision 060d4a512c8521a8df44895c3c233dee3ed00795)
+++ src/routes/guardian.js	(revision 6fab3f3e28add90b1140273d9a28793236cd7a39)
@@ -182,10 +182,45 @@
 
 // ── JSON state for refreshes ─────────────────────────────────────────────
-router.get('/api/state', requireAuth, (req, res) => {
-  const site = siteForUser(req);
-  if (!site) return res.status(404).json({ error: 'no_site' });
-  // Het paneel tikt elke 45 seconden, of er nu iets gebeurd is of niet. Met een
-  // ETag kost stilte een lege 304 in plaats van het hele paneel (Barts punt, 9-8).
-  return AP.sendMaybe304(req, res, dashboardState(site, resolveLang(req)), { contentType: 'application/json' });
+/**
+ * De staat van het paneel, desgewenst als LANGE POLL (Barts opdracht, 9-8).
+ *
+ * Zonder `wait` gedraagt de route zich exact zoals altijd. Met `wait` blijft het
+ * antwoord hangen tot er iets gebeurt dat de guardian moet verwerken, of tot de
+ * tijd om is -- dan een lege 304.
+ *
+ * EERST KIJKEN, DAN WACHTEN. Veranderde er iets tussen het vorige antwoord en
+ * dit verzoek, dan is de merksteen nu al anders en gaat het antwoord METEEN de
+ * deur uit. Zou je eerst gaan wachten, dan blijft nieuws dat net in dat gaatje
+ * viel vijfentwintig seconden liggen -- en juist bij een hulpvraag is dat de
+ * verkeerde vertraging.
+ *
+ * WAKKER OP ALLES. De guardianship-module zendt veertien soorten gebeurtenissen
+ * uit en die wekken allemaal (wakeGuardian); daarnaast wekt de tijdlijn (onNews),
+ * want de berichten van je wards staan in ditzelfde scherm.
+ */
+router.get('/api/state', requireAuth, async (req, res) => {
+  const site = siteForUser(req);
+  if (!site) return res.status(404).json({ error: 'no_site' });
+  const stuur = () => AP.sendMaybe304(req, res, dashboardState(site, resolveLang(req)), { contentType: 'application/json' });
+
+  const wachtS = Math.min(Math.max(parseInt(req.query.wait, 10) || 0, 0), 50);
+  const merk = req.headers['if-none-match'];
+  if (!wachtS || !merk) return stuur();
+
+  // Is er nu al iets anders? Dan niet wachten.
+  const nu = AP.etagFor(JSON.stringify(dashboardState(site, resolveLang(req))));
+  if (nu !== merk) return stuur();
+
+  await new Promise((klaar) => {
+    let af = false;
+    const eind = () => { if (af) return; af = true; clearTimeout(t); offG(); offN(); klaar(); };
+    const offG = AP.onGuardian(site.slug, eind);
+    const offN = AP.onNews(site.slug, eind);
+    const t = setTimeout(eind, wachtS * 1000);
+    // Hing de client op, dan houdt niemand dit antwoord meer vast.
+    res.on('close', eind);
+  });
+  if (res.writableEnded) return undefined;
+  return stuur();
 });
 
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 060d4a512c8521a8df44895c3c233dee3ed00795)
+++ src/services/ActivityPubService.js	(revision 6fab3f3e28add90b1140273d9a28793236cd7a39)
@@ -1860,4 +1860,32 @@
   return () => { set.delete(cb); if (!set.size) _newsWaiters.delete(slug); };
 }
+/**
+ * Wachters op het Guardian-paneel (Barts opdracht, 9-8).
+ *
+ * APART VAN onNews, en dat is met opzet. `news` gaat over de tijdlijn; dit gaat
+ * over alles wat een guardian te VERWERKEN krijgt -- een aanbod, een
+ * volgverzoek, een gate-voorstel, een hulpvraag, een lapse. De guardianship-
+ * module zendt daar al veertien soorten voor uit; die gingen alleen naar push,
+ * en push kiest bewust maar een handvol. Het paneel moet ze allemaal weten.
+ *
+ * Een wachter wordt EEN keer gewekt en daarna vergeten: het antwoord dat volgt
+ * is de nieuwe waarheid, en de client komt terug met een nieuwe wachter.
+ */
+const _guardWaiters = new Map();   // slug -> Set<cb>
+export function onGuardian(slug, cb) {
+  let set = _guardWaiters.get(slug);
+  if (!set) { set = new Set(); _guardWaiters.set(slug, set); }
+  set.add(cb);
+  return () => { set.delete(cb); if (!set.size) _guardWaiters.delete(slug); };
+}
+export function wakeGuardian(slug) {
+  const set = _guardWaiters.get(slug);
+  if (!set || !set.size) return;
+  const cbs = [...set];
+  set.clear();
+  _guardWaiters.delete(slug);
+  for (const cb of cbs) { try { cb(); } catch { /* een wachter mag de rest nooit breken */ } }
+}
+
 export function wakeNews(slug) {
   const set = _newsWaiters.get(slug);
@@ -2387,4 +2415,5 @@
         const ai = actorInfo(await resolveActor(actorUri).catch(() => null), actorUri);
         Guardianship.help.record(mark.noteUri, actorUri, mark.kind, ai && ai.handle);
+        wakeGuardian(slug);   // een mede-guardian pakte iets op: het paneel hoort het meteen
         console.log('[AP] help', mark.kind, actorUri, '→', mark.noteUri);
       }
@@ -2429,4 +2458,5 @@
               if (!mijn) { console.warn('[AP] gate request from someone who is not our ward, ignored:', actorUri, '→', slug); continue; }
               Guardianship.gatereq.record(slug, actorUri, req.feature, o.id);
+              wakeGuardian(slug);   // het kind vroeg om een poort
               console.log('[AP] gate request', req.feature, actorUri, '→', slug);
             }
@@ -5970,9 +6000,24 @@
   // De labels hangen aan dezelfde sleutels als het Guardian-paneel, zodat een
   // melding en het scherm waar hij heen wijst hetzelfde woord gebruiken.
-  onEvent: (slug, ev) => {
-    const p = guardianEventPush(slug, ev);
-    if (p) pushEvent(slug, p);
-  },
+  onEvent: (slug, ev) => onGuardianshipEvent(slug, ev),
 });
+
+/**
+ * Wat er gebeurt als de guardianship-module iets uitzendt.
+ *
+ * TWEE VERSCHILLENDE VRAGEN, en ze horen niet dezelfde te zijn: wie maak je
+ * WAKKER (push kiest bewust een handvol soorten), en wat moet een scherm dat
+ * openstaat WETEN (alles). Het paneel werd daarom voorheen niet gewekt door de
+ * tien soorten zonder pushtekst -- die zag je pas bij de volgende tik.
+ *
+ * Apart en met een naam, zodat een toets erbij kan. Verstopt in de deps-literal
+ * was hij onbereikbaar, en een mutatie die het wekken weghaalde bleef groen.
+ */
+export function onGuardianshipEvent(slug, ev) {
+  wakeGuardian(slug);
+  const p = guardianEventPush(slug, ev);
+  if (p) pushEvent(slug, p);
+  return p;
+}
 
 /**
@@ -6066,5 +6111,5 @@
   getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
   deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker,
-  sendMaybe304, etagFor, proposeGate, getReplyUris, getThread, filterThreadToCircle, gateAttachments, stripEmojiTags, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio,
+  sendMaybe304, etagFor, onGuardian, wakeGuardian, onGuardianshipEvent, proposeGate, getReplyUris, getThread, filterThreadToCircle, gateAttachments, stripEmojiTags, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio,
   linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections,
   noteVisibility, belongsInTimeline, playerUrlFor, isRejectedObject, rejectInteraction, interactionReportTarget,
Index: test/guardian-longpoll.test.js
===================================================================
--- test/guardian-longpoll.test.js	(revision 6fab3f3e28add90b1140273d9a28793236cd7a39)
+++ test/guardian-longpoll.test.js	(revision 6fab3f3e28add90b1140273d9a28793236cd7a39)
@@ -0,0 +1,97 @@
+// De wekker onder het Guardian-paneel (Barts opdracht, 9-8).
+//
+// Het paneel tikte elke 45 seconden, ongeacht of er iets gebeurd was. Nu hangt
+// er een verzoek open tot er iets is dat de guardian moet verwerken.
+//
+// Wat hier bewaakt wordt is de BUS, niet de route: dat elke soort gebeurtenis
+// wekt, dat een wachter maar EEN keer gewekt wordt, en dat een wachter die zich
+// afmeldt ook echt weg is. Een lange poll die niet wekt is niet te onderscheiden
+// van een trage server -- en dat is precies het soort fout dat niemand meldt.
+//
+// Run: npm test
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+const AP = await import('../src/services/ActivityPubService.js');
+
+test('een wachter wordt gewekt', () => {
+  let gewekt = 0;
+  AP.onGuardian('kind', () => { gewekt += 1; });
+  AP.wakeGuardian('kind');
+  assert.equal(gewekt, 1);
+});
+
+test('en daarna vergeten: een tweede wek doet niets', () => {
+  // Het antwoord dat volgt is de nieuwe waarheid; de client komt terug met een
+  // nieuwe wachter. Bleef hij staan, dan zou een burst van tien gebeurtenissen
+  // tien antwoorden op hetzelfde verzoek proberen te schrijven.
+  let gewekt = 0;
+  AP.onGuardian('kind2', () => { gewekt += 1; });
+  AP.wakeGuardian('kind2');
+  AP.wakeGuardian('kind2');
+  assert.equal(gewekt, 1);
+});
+
+test('afmelden werkt', () => {
+  // Hangt de client op, dan moet de wachter weg. Anders lekt elke afgebroken
+  // verbinding een callback, en bij een lange poll is dat elke 25 seconden een.
+  let gewekt = 0;
+  const af = AP.onGuardian('kind3', () => { gewekt += 1; });
+  af();
+  AP.wakeGuardian('kind3');
+  assert.equal(gewekt, 0);
+});
+
+test('wekken raakt alleen de eigen guardian', () => {
+  let a = 0; let b = 0;
+  AP.onGuardian('oma', () => { a += 1; });
+  AP.onGuardian('opa', () => { b += 1; });
+  AP.wakeGuardian('oma');
+  assert.equal(a, 1);
+  assert.equal(b, 0);
+});
+
+test('meerdere schermen van dezelfde guardian worden allemaal gewekt', () => {
+  // Telefoon en laptop naast elkaar. Zou alleen de laatste wachter gewekt
+  // worden, dan blijft het andere scherm stil staan.
+  let n = 0;
+  AP.onGuardian('duo', () => { n += 1; });
+  AP.onGuardian('duo', () => { n += 1; });
+  AP.wakeGuardian('duo');
+  assert.equal(n, 2);
+});
+
+test('een wachter die stukgaat breekt de rest niet', () => {
+  // Een kapotte callback mag nooit de andere schermen laten hangen.
+  let goed = 0;
+  AP.onGuardian('stuk', () => { throw new Error('boem'); });
+  AP.onGuardian('stuk', () => { goed += 1; });
+  AP.wakeGuardian('stuk');
+  assert.equal(goed, 1);
+});
+
+test('wekken zonder wachters is geen fout', () => {
+  assert.doesNotThrow(() => AP.wakeGuardian('niemand'));
+});
+
+test('ELKE guardianship-gebeurtenis wekt, ook die geen melding waard is', () => {
+  // De kern van Barts opdracht: push kiest bewust een handvol soorten, het
+  // scherm hoort ze allemaal te weten. Dit gaat door de ECHTE handler heen --
+  // een eerdere versie toetste alleen de bus, en toen bleef de mutatie die het
+  // wekken uit onEvent haalde gewoon groen.
+  const ev = { kind: 'lapse_vote', lapse: 'x', by: 'y', state: 'recorded' };
+  assert.equal(AP.guardianEventPush('kind9', ev), null, 'deze soort geeft geen melding');
+
+  let gewekt = 0;
+  AP.onGuardian('kind9', () => { gewekt += 1; });
+  AP.onGuardianshipEvent('kind9', ev);
+  assert.equal(gewekt, 1, 'en toch wordt het paneel gewekt');
+});
+
+test('een gebeurtenis die WEL een melding is, wekt ook', () => {
+  let gewekt = 0;
+  AP.onGuardian('kind10', () => { gewekt += 1; });
+  AP.onGuardianshipEvent('kind10', { kind: 'committed', ward: 'https://w/x' });
+  assert.equal(gewekt, 1);
+});
