Index: src/services/CircleService.js
===================================================================
--- src/services/CircleService.js	(revision 814ad8b02937ff28899c1d361333322528296fbe)
+++ src/services/CircleService.js	(revision d7746798ec30f65765731ccbef3852624ec58f96)
@@ -1,7 +1,7 @@
-// CircleService.js — pull-kant van Cirkels (v1).
+// CircleService.js — pull side of Circles (v1).
 //
-// Haalt per circle_link de remote actor + outbox op, verifieert de Ed25519-
-// handtekening, sanitiseert en cachet publieke posts in remote_actors/remote_posts.
-// Alleen LEZEN van remote; nooit schrijven. Zie docs/cirkels-v1-spec.md §5b.
+// Per circle_link: fetches the remote actor + outbox, verifies the Ed25519
+// signature, sanitizes, and caches public posts in remote_actors/remote_posts.
+// READ ONLY from remote; never write. See docs/cirkels-v1-spec.md §5b.
 
 import db from '../config/database.js';
@@ -16,5 +16,5 @@
   return String(s || '')
     .replace(/<[^>]+>/g, ' ')
-    .replace(/\[\[[^\]]*\]\]/g, ' ')   // [[playlist:..]]/[[track:..]]/[[album:..]]-shortcodes weg
+    .replace(/\[\[[^\]]*\]\]/g, ' ')   // strip [[playlist:..]] / [[track:..]] / [[album:..]] shortcodes
     .replace(/\s+/g, ' ')
     .trim();
@@ -31,5 +31,5 @@
 }
 
-// AS Hashtag-array -> comma-separated tagnamen (zonder #), gesanitized.
+// AS Hashtag array -> comma-separated tag names (without #), sanitized.
 function extractTags(tag) {
   if (!Array.isArray(tag)) return null;
@@ -41,10 +41,10 @@
 }
 
-// Bron buiten de cirkel zetten met een leesbare reden (geen stille mislukking).
-// Aparte status 'outdated' zodat de Beheer-UI er een nette "update vereist"-
-// melding van kan maken i.p.v. een generieke fout.
+// Mark a source as outside the circle with a readable reason (no silent failure).
+// Separate 'outdated' status so the admin UI can show a clean "update required"
+// notice instead of a generic error.
 function markOutdated(link, msg) {
-  // Gecachte posts van deze bron weghalen: we kunnen ze niet meer verifiëren of
-  // verversen (proto-mismatch), dus ze horen niet meer in de cirkel-feed.
+  // Remove cached posts from this source: we can no longer verify or refresh
+  // them (proto mismatch), so they no longer belong in the circle feed.
   if (link.remote_actor_id) {
     try { db.prepare('DELETE FROM remote_posts WHERE actor_id = ?').run(link.remote_actor_id); } catch {}
@@ -55,5 +55,5 @@
 }
 
-// Robuuste, defensieve fetch: alleen https, timeout, body-cap, redirect-follow.
+// Robust, defensive fetch: https only, timeout, body cap, redirect follow.
 async function fetchText(url) {
   if (!/^https:\/\//i.test(url)) throw new Error('alleen https toegestaan');
@@ -66,5 +66,5 @@
       headers: {
         Accept: 'application/activity+json, application/json',
-        // Vertel de publisher onze proto → die kan ons met 426 weren als we te oud zijn.
+        // Tell the publisher our proto → they can reject us with 426 if we are too old.
         'Klonkt-Proto': String(KLONKT_PROTO),
       },
@@ -72,5 +72,5 @@
     if (!res.ok) throw new Error(`HTTP ${res.status}`);
     const buf = Buffer.from(await res.arrayBuffer());
-    if (buf.length > MAX_BODY_BYTES) throw new Error('body te groot');
+    if (buf.length > MAX_BODY_BYTES) throw new Error('body too large');
     return { text: buf.toString('utf8'), headers: res.headers, finalUrl: res.url };
   } finally {
@@ -79,6 +79,6 @@
 }
 
-// Lazy prepares — de tabellen bestaan pas ná initializeDatabase(); dit module
-// wordt geïmporteerd vóór die call, dus niet op module-niveau prepare'n.
+// Lazy prepares — tables only exist after initializeDatabase(); this module is
+// imported before that call, so do not prepare at module level.
 let _stmts = null;
 function stmts() {
@@ -107,5 +107,5 @@
   const base = baseOf(link.remote_url);
 
-  // 1. Actor ophalen + valideren
+  // 1. Fetch + validate actor
   const actorUrl = `${base}/.klonkt/actor.json`;
   const a = await fetchText(actorUrl);
@@ -117,8 +117,8 @@
   if (originOf(actorId) !== originOf(actorUrl)) throw new Error('actor.id heeft andere origin dan de actor-URL');
 
-  // Protocol-versie-gate. De proto zit óók in de outbox-handtekening-grondslag,
-  // dus liegen in de (ongetekende) actor helpt niet: bij een echte mismatch faalt
-  // de verificatie verderop alsnog. Hier vooral voor een DUIDELIJKE melding +
-  // buitensluiten zonder stille mislukking.
+  // Protocol version gate. The proto is also embedded in the outbox signing
+  // input, so lying in the (unsigned) actor does not help: a real mismatch
+  // will still fail verification later. This check is mainly for a CLEAR
+  // message + exclusion without silent failure.
   const remoteProto = Number(actor.klonkt && actor.klonkt.proto) || 1;
   if (remoteProto > KLONKT_PROTO) {
@@ -131,5 +131,5 @@
   }
 
-  // TOFU: een sleutelwissel vereist expliciete herbevestiging (anti-hijack)
+  // TOFU: a key change requires explicit re-confirmation (anti-hijack)
   const existing = db.prepare('SELECT public_key FROM remote_actors WHERE id = ?').get(actorId);
   if (existing && existing.public_key !== pubKey) {
@@ -146,5 +146,5 @@
   });
 
-  // 2. Outbox ophalen + handtekening verifiëren
+  // 2. Fetch outbox + verify signature
   const outboxUrl = actor.outbox || `${base}/.klonkt/outbox.json`;
   const o = await fetchText(outboxUrl);
@@ -158,5 +158,5 @@
   const items = Array.isArray(outbox.orderedItems) ? outbox.orderedItems.slice(0, MAX_ITEMS) : [];
 
-  // 3. Objecten sanitizen + cachen (same-origin als de actor = anti-impersonatie)
+  // 3. Sanitize + cache objects (same origin as actor = anti-impersonation)
   const actorOrigin = originOf(actorId);
   const seen = new Set();
@@ -186,5 +186,5 @@
   }
 
-  // 4. Pruning: posts die niet meer in de outbox staan opruimen
+  // 4. Pruning: remove posts that are no longer in the outbox
   const known = db.prepare('SELECT id FROM remote_posts WHERE actor_id = ?').all(actorId).map((r) => r.id);
   const stale = known.filter((id) => !seen.has(id));
@@ -194,6 +194,6 @@
   }
 
-  // Naam automatisch overnemen van de remote actor (geen handmatige invoer nodig).
-  // COALESCE: heeft de actor geen naam, dan blijft een evt. bestaand label staan.
+  // Automatically adopt the name from the remote actor (no manual entry needed).
+  // COALESCE: if the actor has no name, any existing label is preserved.
   db.prepare(
     "UPDATE circle_links SET remote_actor_id=?, label=COALESCE(?, label), last_synced=CURRENT_TIMESTAMP, status='active', last_error=NULL WHERE id=?"
@@ -221,9 +221,9 @@
 
 let _timer = null;
-/** Periodieke achtergrond-sync (gated op tenancy='circle' binnen sync()). */
+/** Periodic background sync (gated on tenancy='circle' inside sync()). */
 export function startCircleSyncLoop(intervalMs = 15 * 60 * 1000) {
   if (_timer) return;
   const run = () => { sync().catch((e) => console.error('[cirkels] sync-fout:', e.message)); };
-  setTimeout(run, 30 * 1000); // korte delay na boot
+  setTimeout(run, 30 * 1000); // short delay after boot
   _timer = setInterval(run, intervalMs);
   if (_timer.unref) _timer.unref();
