Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 0b0cd54af139c41ca90ad8f48c8d5532aa692616)
+++ src/routes/activitypub.js	(revision 86e6a45456a85581974a889cf6f6eccef3c18081)
@@ -314,4 +314,8 @@
 // guardians' real states here and nowhere else.
 queueRoute('guardians', (id, slug) => Guardianship.guardiansCollection(id, slug));
+// De hulpvragen MET hun staat (5.2.1, shaer-lgo). De apps lazen ze uit de feed
+// en wisten dus niet of er al iemand op af was -- daarom bleef een afgehandeld
+// verzoek daar staan (Barts melding, 8-8).
+queueRoute('help', (id, slug) => Guardianship.helpCollection(id, slug));
 
 // ── Inbox read (owner only, AP C2S) ───────────────────────────────
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 0b0cd54af139c41ca90ad8f48c8d5532aa692616)
+++ src/services/ActivityPubService.js	(revision 86e6a45456a85581974a889cf6f6eccef3c18081)
@@ -3344,5 +3344,18 @@
           }
           const gateReq = Guardianship.gatereq.parseRequest(object);
-          const r = await deliverDirectNote(site, { recipients, text: plain, language: object.language || null, inReplyTo: typeof object.inReplyTo === 'string' ? object.inReplyTo : null, attachments: atts, helpRequest: help, awayUntil, gateRequest: gateReq && gateReq.feature });
+          // Een hulpvraag oppikken of afsluiten vanuit de app (5.2.1, shaer-lgo).
+          // De markering IS al een gewone directe note met een shaer:-eigenschap,
+          // dus hier hoeft niets nieuws bij: de app stuurt precies wat de PWA
+          // stuurt, en het gaat over dezelfde bezorging naar de mede-guardians.
+          //
+          // We boeken hem ook LOKAAL. Zonder dat zou de guardian die de knop
+          // indrukt zijn eigen markering pas zien als hij bij zichzelf
+          // terugkomt -- en die weg bestaat niet.
+          const mark = Guardianship.help.parseMarker(object);
+          if (mark) {
+            const base2 = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
+            Guardianship.help.record(mark.noteUri, actorId(base2, site.slug), mark.kind, `@${site.slug}`);
+          }
+          const r = await deliverDirectNote(site, { recipients, text: plain, language: object.language || null, inReplyTo: typeof object.inReplyTo === 'string' ? object.inReplyTo : null, attachments: atts, helpRequest: help, awayUntil, gateRequest: gateReq && gateReq.feature, helpMark: mark });
           if (!r || !r.id) return { status: 502, error: 'direct_failed' };
           return { status: 201, id: r.id, url: `${base}/ap/notes/${r.id}` };
Index: src/services/guardianship/index.js
===================================================================
--- src/services/guardianship/index.js	(revision 0b0cd54af139c41ca90ad8f48c8d5532aa692616)
+++ src/services/guardianship/index.js	(revision 86e6a45456a85581974a889cf6f6eccef3c18081)
@@ -19,5 +19,5 @@
 export { wireDelivery, c2sVisibility, deliverDirectNote } from './delivery.js';
 export { wireHandshake, handleOutbox as handleGuardianshipOutbox, handleInbox as handleGuardianshipInbox, parseRelationship, parseUndoRelationship, endGuardianship } from './handshake.js';
-export { offersCollection, followsCollection, outgoingFollowsCollection, wardsCollection, guardiansCollection } from './queues.js';
+export { offersCollection, followsCollection, outgoingFollowsCollection, wardsCollection, guardiansCollection, helpCollection } from './queues.js';
 export * as availability from './availability.js';
 export { wireAvailability } from './availability.js';
Index: src/services/guardianship/queues.js
===================================================================
--- src/services/guardianship/queues.js	(revision 0b0cd54af139c41ca90ad8f48c8d5532aa692616)
+++ src/services/guardianship/queues.js	(revision 86e6a45456a85581974a889cf6f6eccef3c18081)
@@ -16,4 +16,6 @@
 import * as gated from './gated.js';
 import * as gatereq from './gatereq.js';
+import * as help from './help.js';
+import db from '../../config/database.js';
 import * as handshake from './handshake.js';
 
@@ -116,5 +118,5 @@
 }
 
-export default { offersCollection, followsCollection, outgoingFollowsCollection, wardsCollection, guardiansCollection, wardGates, wardGuardianStatuses };
+export default { offersCollection, followsCollection, outgoingFollowsCollection, wardsCollection, guardiansCollection, helpCollection, helpItemsFor, wardGates, wardGuardianStatuses };
 
 // ── Wat er voor een ward gated is (shaer-ahy.1) ─────────────────────────
@@ -179,2 +181,54 @@
   });
 }
+
+
+// ── Hulpvragen met hun staat (shaer-lgo, shaer-ahy.1) ───────────────────
+//
+// De PWA had dit al; de apps kregen alleen de losse notes uit de feed en wisten
+// dus NIET of er al iemand op af was. Daarom bleef een afgehandeld verzoek daar
+// gewoon staan -- Barts melding. De staat wordt hier een keer berekend, zoals bij
+// wardGates: twee berekeningen zouden twee guardians een ander beeld geven van
+// hetzelfde kind.
+
+/** De hulpvragen van deze guardian, met wie erop af is en of het dicht is. */
+export function helpItemsFor(slug, limit = 50) {
+  let rijen = [];
+  try {
+    rijen = db.prepare(
+      `SELECT object_uri, actor_uri, actor_name, actor_handle, actor_icon, content, published, created_at
+       FROM ap_mentions WHERE slug = ? AND help_request = 1 ORDER BY created_at DESC LIMIT ?`,
+    ).all(slug, limit);
+  } catch { return []; }
+  const staat = help.statusFor(rijen.map((r) => r.object_uri));
+  const mijn = new Set(relations.listWards(slug).map((w) => w.other_uri));
+  return rijen.map((r) => ({
+    ...r,
+    // Bij twijfel OPEN. Een hulpvraag die er afgehandeld uitziet terwijl hij dat
+    // niet is, is de gevaarlijke fout -- niet andersom.
+    state: help.withWardship(
+      staat.get(r.object_uri) || { open: true, pickedUpBy: [], handled: null, ageMs: null },
+      mijn.has(r.actor_uri),
+    ),
+  }));
+}
+
+/** Dezelfde vragen als collectie voor de apps (5.2.1). */
+export function helpCollection(id, slug) {
+  const items = helpItemsFor(slug).map((h) => ({
+    id: h.object_uri,
+    type: 'Note',
+    attributedTo: h.actor_uri,
+    'shaer:handle': h.actor_handle || undefined,
+    content: h.content || '',
+    published: h.published || h.created_at,
+    'shaer:helpRequest': true,
+    // De staat als platte velden: een app hoeft hem niet af te leiden, en kan
+    // hem dus ook niet anders afleiden dan het paneel.
+    'shaer:open': h.state.open,
+    'shaer:handledBy': h.state.handled ? (h.state.handled.handle || h.state.handled.uri) : undefined,
+    'shaer:handledAt': h.state.handled ? h.state.handled.at : undefined,
+    'shaer:pickedUpBy': h.state.pickedUpBy.map((p) => p.handle || p.uri),
+    'shaer:formerWard': h.state.formerWard || undefined,
+  }));
+  return collection(id, items);
+}
Index: src/services/guardianship/relations.js
===================================================================
--- src/services/guardianship/relations.js	(revision 0b0cd54af139c41ca90ad8f48c8d5532aa692616)
+++ src/services/guardianship/relations.js	(revision 86e6a45456a85581974a889cf6f6eccef3c18081)
@@ -80,4 +80,5 @@
       wards: `${id}/queues/wards`,
       guardians: `${id}/queues/guardians`,
+      help: `${id}/queues/help`,
     },
   };
Index: test/activitypub-as2.test.js
===================================================================
--- test/activitypub-as2.test.js	(revision 0b0cd54af139c41ca90ad8f48c8d5532aa692616)
+++ test/activitypub-as2.test.js	(revision 86e6a45456a85581974a889cf6f6eccef3c18081)
@@ -40,5 +40,7 @@
   // `outgoingFollows` is §5.3 turned around: the ward's own follow requests,
   // waiting for the guardians (shaer-p729).
-  'shaer:queues', 'offers', 'follows', 'outgoingFollows', 'wards', 'guardians',
+  // `help` is de vragenlijst met haar STAAT (5.2.1): de apps lazen hulpvragen uit
+  // de feed en wisten niet of er al iemand op af was.
+  'shaer:queues', 'offers', 'follows', 'outgoingFollows', 'wards', 'guardians', 'help',
   // ActivityPub §4.1 `endpoints` vocabulary (same category as sharedInbox), used for C2S.
   'oauthAuthorizationEndpoint', 'oauthTokenEndpoint', 'uploadMedia',
Index: test/help-c2s.test.js
===================================================================
--- test/help-c2s.test.js	(revision 86e6a45456a85581974a889cf6f6eccef3c18081)
+++ test/help-c2s.test.js	(revision 86e6a45456a85581974a889cf6f6eccef3c18081)
@@ -0,0 +1,84 @@
+// Een hulpvraag oppikken en afsluiten vanuit de app (shaer-lgo, Barts melding 8-8).
+//
+// De apps lazen hulpvragen uit de FEED en kregen de staat niet mee. Ze wisten dus
+// niet of er al iemand op af was, en een afgehandeld verzoek bleef gewoon staan.
+//
+// Wat hier bewaakt wordt: de app loopt door dezelfde markering en dezelfde staat
+// als het paneel. Twee berekeningen zouden twee guardians een ander beeld geven
+// van hetzelfde kind -- en bij een reddingsboei is dat het gevaarlijkste dat er
+// mis kan gaan.
+//
+// In-memory SQLite. Run: npm test
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://oma.test';
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+dbMod.initializeDatabase();
+const AP = await import('../src/services/ActivityPubService.js');
+const queues = await import('../src/services/guardianship/queues.js');
+const rel = await import('../src/services/guardianship/relations.js');
+
+db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+  .run('u1', 'u1', 'u1@t', 'x', 'god');
+db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'oma', 'Oma', 'u1');
+const site = () => db.prepare('SELECT * FROM sites WHERE id = ?').get('s1');
+const user = { id: 'u1', username: 'u1' };
+const KIND = 'https://elders.test/ap/users/kind';
+const BOEI = 'https://elders.test/ap/notes/boei-1';
+rel.commitWardForGuardian('oma', KIND, { handle: '@kind' });
+db.prepare(`INSERT INTO ap_mentions (slug, object_uri, actor_uri, actor_handle, content, help_request)
+            VALUES (?,?,?,?,?,1)`).run('oma', BOEI, KIND, '@kind', '<p>help</p>');
+
+const marker = (kind) => ({
+  type: 'Create',
+  object: {
+    type: 'Note',
+    content: '<p>.</p>',
+    to: [KIND],
+    [kind === 'handled' ? 'shaer:helpHandled' : 'shaer:helpPickup']: BOEI,
+  },
+});
+
+test('een verse hulpvraag staat OPEN', () => {
+  const h = queues.helpItemsFor('oma').find((x) => x.object_uri === BOEI);
+  assert.equal(h.state.open, true);
+  assert.equal(h.state.handled, null);
+});
+
+test('oppikken vanuit de app landt in dezelfde staat als het paneel', async () => {
+  await AP.ingestOutboxActivity(site(), user, marker('pickup'));
+  const h = queues.helpItemsFor('oma').find((x) => x.object_uri === BOEI);
+  assert.equal(h.state.pickedUpBy.length, 1);
+  // Nog steeds open: oppikken is geen afhandelen. De faalstand hier is
+  // "iedereen denkt dat het geregeld is", en die is gevaarlijker dan geen
+  // markering.
+  assert.equal(h.state.open, true);
+});
+
+test('afsluiten vanuit de app sluit hem ook echt', async () => {
+  await AP.ingestOutboxActivity(site(), user, marker('handled'));
+  const h = queues.helpItemsFor('oma').find((x) => x.object_uri === BOEI);
+  assert.equal(h.state.open, false);
+  assert.ok(h.state.handled, 'met wie het deed erbij');
+});
+
+test('de collectie voor de apps draagt de staat plat mee', () => {
+  // Een app hoeft hem niet af te leiden, en kan hem dus ook niet anders
+  // afleiden dan het paneel.
+  const coll = queues.helpCollection('https://x/queues/help', 'oma');
+  const item = coll.orderedItems.find((i) => i.id === BOEI);
+  assert.equal(item['shaer:open'], false);
+  assert.ok(item['shaer:handledBy']);
+  assert.equal(item['shaer:helpRequest'], true);
+});
+
+test('een note zonder markering blijft een gewoon bericht', async () => {
+  const uit = await AP.ingestOutboxActivity(site(), user, {
+    type: 'Create', object: { type: 'Note', content: '<p>hoi</p>', to: [KIND] },
+  });
+  assert.notEqual(uit.status, 400);
+});
