Index: src/routes/feed.js
===================================================================
--- src/routes/feed.js	(revision 09fc5fb298f25f61bef8fa385fdf73f1c2c7781f)
+++ src/routes/feed.js	(revision f549cf53247e0d8d7ccbd330dac9f3f4040c3e92)
@@ -44,5 +44,24 @@
 
 // ==================== RSS 2.0 ====================
-router.get('/feed.xml', (req, res) => {
+/**
+ * Twee feeds, een bouwer.
+ *
+ *   /feed.xml    de site: posts EN open tracks door elkaar, chronologisch
+ *   /tracks.xml  alleen de open tracks -- de hele site als muziekkanaal
+ *
+ * WAAROM DIE TWEEDE (Robins vraag, 10-8). Hij vroeg naar een "hele site, open
+ * tracks"-library voor Funkwhale. Als AS2-object helpt zo'n ding daar niet: een
+ * Funkwhale-KANAAL wijst nergens naar een library, en hun library is een eigen
+ * actor met inbox en sleutel -- de optie die op 7 augustus is afgewezen. Maar de
+ * vorm waarin Funkwhale een kanaal WEL uitgeeft is RSS met iTunes en een
+ * enclosure per item, en dat is precies wat Klonkt hier al doet. Naast een echte
+ * kanaalfeed gelegd (audio.pepemoss.com/api/v1/channels/tnd/rss): dezelfde
+ * namespaces, dezelfde enclosure.
+ *
+ * Het enige verschil was dat onze feed ook gewone posts draagt. Een muziekkanaal
+ * met blogberichten ertussen is er geen, dus die feed staat er nu apart -- een
+ * URL om te plakken, zonder dat er iets naar buiten geduwd wordt.
+ */
+function stuurFeed(req, res, { alleenTracks }) {
   const site = res.locals.site;
   if (!site) return res.status(404).send('No site');
@@ -50,5 +69,5 @@
   const origin = siteOrigin(req);
   const base = origin + (res.locals.siteUrlBase || '');
-  const posts = postsForFeed(site.id);
+  const posts = alleenTracks ? [] : postsForFeed(site.id);
 
   // De tracks die deze site aan de federatie heeft opengezet, elk als eigen
@@ -65,5 +84,5 @@
   // poortregel en die hoort op een plek te staan.
   const tracks = siteOpenTracks(site.id);
-  const lastBuild = posts[0]?.published_at || new Date().toISOString();
+  const lastBuild = posts[0]?.published_at || tracks[0]?.created_at || new Date().toISOString();
 
   // De itunes-velden waar een podcast-app een kanaal aan herkent. Funkwhale
@@ -117,15 +136,19 @@
 <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
   <channel>
-    <title>${escapeXml(site.title)}</title>
+    <title>${escapeXml(alleenTracks ? `${site.title} \u2014 muziek` : site.title)}</title>
     <link>${escapeXml(base + '/')}</link>
     <description>${escapeXml(site.description || site.tagline || '')}</description>
     <language>${escapeXml(site.language || 'nl')}</language>
     <lastBuildDate>${new Date(lastBuild).toUTCString()}</lastBuildDate>
-    <atom:link href="${escapeXml(base + '/feed.xml')}" rel="self" type="application/rss+xml" />
+    <atom:link href="${escapeXml(base + (alleenTracks ? '/tracks.xml' : '/feed.xml'))}" rel="self" type="application/rss+xml" />
 ${kanaalTags}
 ${items}
   </channel>
 </rss>`);
-});
+}
+
+router.get('/feed.xml', (req, res) => stuurFeed(req, res, { alleenTracks: false }));
+// De hele site als muziekkanaal: alleen wat op de federatie openstaat.
+router.get('/tracks.xml', (req, res) => stuurFeed(req, res, { alleenTracks: true }));
 
 // ==================== Atom 1.0 ====================
Index: src/services/guardianship/queues.js
===================================================================
--- src/services/guardianship/queues.js	(revision 09fc5fb298f25f61bef8fa385fdf73f1c2c7781f)
+++ src/services/guardianship/queues.js	(revision f549cf53247e0d8d7ccbd330dac9f3f4040c3e92)
@@ -294,4 +294,15 @@
     'shaer:handledAt': h.state.handled ? h.state.handled.at : undefined,
     'shaer:pickedUpBy': h.state.pickedUpBy.map((p) => p.handle || p.uri),
+    // HOE OUD het oudste oppakken is (shaer-lgo). Barts besluit was dat
+    // "opgepikt" niet vervalt maar zichtbaar VEROUDERT -- het verschil tussen
+    // "er is iemand mee bezig" en "er was ooit iemand mee bezig". Het paneel
+    // toonde dat al; de apps konden het niet, want dit veld bleef hier liggen.
+    // Een oppak van vijf minuten zag er daar uit als een van vijf dagen, en de
+    // faalstand is hier nou juist "iedereen denkt dat het geregeld is".
+    //
+    // Als TIJDSTIP en niet als leeftijd: een leeftijd maakt elk antwoord anders
+    // en dan kan de ETag nooit gelijk zijn. De client rekent zelf terug, precies
+    // zoals guardian.js het doet.
+    'shaer:oldestPickupAt': h.state.oldestPickupAt || undefined,
     'shaer:formerWard': h.state.formerWard || undefined,
   }));
Index: test/feed-tracks-only.test.js
===================================================================
--- test/feed-tracks-only.test.js	(revision f549cf53247e0d8d7ccbd330dac9f3f4040c3e92)
+++ test/feed-tracks-only.test.js	(revision f549cf53247e0d8d7ccbd330dac9f3f4040c3e92)
@@ -0,0 +1,111 @@
+// /tracks.xml -- de hele site als muziekkanaal (Robins vraag, 10-8).
+//
+// Hij vroeg naar een "hele site, open tracks"-library voor Funkwhale. Als
+// AS2-object helpt dat daar niet: een Funkwhale-kanaal wijst nergens naar een
+// library. Maar de vorm waarin Funkwhale een kanaal WEL uitgeeft is RSS met
+// iTunes en een enclosure per item, en dat doet Klonkt al -- alleen droeg die
+// feed ook gewone posts, en een muziekkanaal met blogberichten ertussen is er
+// geen. Vandaar een tweede ingang op dezelfde bouwer.
+//
+// Per ITEM lezen en niet met een patroon over het hele document: dat laatste
+// telt een titel in de kanaalkop net zo hard mee als een titel in een item, en
+// dan meet de test iets anders dan hij beweert.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://test.example';
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+dbMod.initializeDatabase();
+const express = (await import('express')).default;
+const feed = (await import('../src/routes/feed.js')).default;
+
+db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+  .run('u1', 'zanger', 'zanger@t', 'x', 'god');
+db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
+  .run('s1', 'band', 'De Band', 'u1');
+
+db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, excerpt, content, status, published_at)
+            VALUES (?,?,?,?,?,?,?,'published',?)`)
+  .run('p1', 's1', 'u1', 'een-blog', 'Een blogbericht', 'kort', '<p>x</p>', '2026-01-01T00:00:00Z');
+
+const insM = db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,?)');
+const insT = db.prepare('INSERT INTO audio_tracks (id, site_id, title, artist, duration, media_id, fedi_open, created_at) VALUES (?,?,?,?,?,?,?,?)');
+insM.run('m1', 's1', 'open.mp3', 'audio/open.mp3', 'audio/mpeg', 4210688);
+insM.run('m2', 's1', 'dicht.mp3', 'audio/dicht.mp3', 'audio/mpeg', 999);
+insT.run('t1', 's1', 'Het open nummer', 'De Band', 203, 'm1', 1, '2026-03-01 12:00:00');
+insT.run('t2', 's1', 'Achter de poort', 'De Band', 100, 'm2', 0, '2026-04-01 12:00:00');
+
+const app = express();
+app.use((req, res, next) => {
+  res.locals.site = db.prepare("SELECT * FROM sites WHERE id = 's1'").get();
+  res.locals.siteUrlBase = '';
+  next();
+});
+app.use(feed);
+const server = app.listen(0);
+await new Promise((r) => server.once('listening', r));
+const poort = server.address().port;
+const haal = async (pad) => (await fetch(`http://127.0.0.1:${poort}${pad}`)).text();
+
+/** Splitsen op <item>-grenzen en dan per item op tagniveau lezen, in plaats
+ *  van een patroon over het hele document laten lopen: dat laatste telt een
+ *  titel in de kop net zo hard mee als een titel in een item. */
+function items(xml) {
+  return [...xml.matchAll(/<item>([\s\S]*?)<\/item>/g)].map((m) => m[1]);
+}
+function tekst(blok, tag) {
+  const m = blok.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`));
+  return m ? m[1] : null;
+}
+
+const muziek = await haal('/tracks.xml');
+const alles = await haal('/feed.xml');
+
+test('de muziekfeed draagt ALLEEN tracks, geen blogberichten', () => {
+  const it = items(muziek);
+  assert.equal(it.length, 1, 'een open track, en de post hoort er niet in');
+  assert.equal(tekst(it[0], 'title'), 'Het open nummer');
+  assert.ok(!muziek.includes('Een blogbericht'), 'de post staat er niet in');
+});
+
+test('en de gewone feed draagt ze allebei nog steeds', () => {
+  const it = items(alles);
+  assert.equal(it.length, 2, 'de sitefeed verandert niet: post en track');
+  assert.ok(alles.includes('Een blogbericht'));
+});
+
+test('een gesloten track blijft ook hier weg -- dicht is dicht', () => {
+  assert.ok(!muziek.includes('Achter de poort'));
+  assert.ok(!muziek.includes('dicht.mp3'));
+});
+
+test('elke item heeft de enclosure die een podcast-lezer zoekt', () => {
+  const enc = muziek.match(/<enclosure[^>]*>/g) || [];
+  assert.equal(enc.length, 1);
+  assert.match(enc[0], /url="[^"]*\/audio\/stream\/open\.mp3"/);
+  assert.match(enc[0], /length="4210688"/);
+  assert.match(enc[0], /type="audio\/mpeg"/);
+});
+
+test('de self-link wijst naar ZICHZELF en niet naar de sitefeed', () => {
+  // Anders stuurt een lezer die de feed opnieuw ophaalt zichzelf naar de
+  // gemengde variant, en staan de blogberichten er alsnog in.
+  assert.match(muziek, /<atom:link href="[^"]*\/tracks\.xml" rel="self"/);
+  assert.ok(!/<atom:link href="[^"]*\/feed\.xml" rel="self"/.test(muziek));
+  assert.match(alles, /<atom:link href="[^"]*\/feed\.xml" rel="self"/);
+});
+
+test('en hij heet anders, want twee gelijknamige feeds zijn niet te scheiden', () => {
+  assert.ok(muziek.includes('De Band — muziek'));
+  assert.ok(alles.includes('<title>De Band</title>'));
+});
+
+test('de iTunes-categorie staat er, want er is echt muziek', () => {
+  assert.match(muziek, /<itunes:category text="Music" \/>/);
+  assert.match(muziek, /<itunes:duration>203<\/itunes:duration>/);
+});
+
+test.after(() => server.close());
Index: test/help-c2s.test.js
===================================================================
--- test/help-c2s.test.js	(revision 09fc5fb298f25f61bef8fa385fdf73f1c2c7781f)
+++ test/help-c2s.test.js	(revision f549cf53247e0d8d7ccbd330dac9f3f4040c3e92)
@@ -60,4 +60,18 @@
 });
 
+test('en de apps krijgen te horen HOE OUD dat oppakken is', () => {
+  // Barts besluit: opgepikt vervalt niet maar veroudert zichtbaar -- het
+  // verschil tussen "er is iemand mee bezig" en "er was ooit iemand mee bezig".
+  // Het paneel toonde dat al uit dezelfde staat; de apps kregen het veld niet,
+  // dus daar zag een oppak van vijf minuten eruit als een van vijf dagen.
+  const coll = queues.helpCollection('https://x/queues/help', 'oma');
+  const item = coll.orderedItems.find((i) => i.id === BOEI);
+  assert.ok(item['shaer:oldestPickupAt'], 'het veld staat er');
+  assert.ok(!Number.isNaN(Date.parse(item['shaer:oldestPickupAt'])), 'en is een leesbaar tijdstip');
+  // Een TIJDSTIP en geen leeftijd: een leeftijd maakt elk antwoord anders en
+  // dan kan de ETag nooit gelijk zijn. De client rekent zelf terug.
+  assert.equal(typeof item['shaer:oldestPickupAt'], 'string');
+});
+
 test('afsluiten vanuit de app sluit hem ook echt', async () => {
   await AP.ingestOutboxActivity(site(), user, marker('handled'));
@@ -75,4 +89,7 @@
   assert.ok(item['shaer:handledBy']);
   assert.equal(item['shaer:helpRequest'], true);
+  // En de ouderdom is WEG zodra hij is afgehandeld: hoe lang er iemand naar
+  // keek is dan geen informatie meer, en zou als "er wacht nog iets" lezen.
+  assert.equal(item['shaer:oldestPickupAt'], undefined);
 });
 
