Index: scripts/backfill-cirkel-covers.mjs
===================================================================
--- scripts/backfill-cirkel-covers.mjs	(revision c628db10ab7264c2115f59bdd80eefa38f5d7983)
+++ scripts/backfill-cirkel-covers.mjs	(revision c628db10ab7264c2115f59bdd80eefa38f5d7983)
@@ -0,0 +1,37 @@
+// Backfill missing Cirkel/timeline covers: re-fetch each coverless ap_timeline
+// note (as ActivityPub) and pull its cover from the now-exposed `image` (or an
+// image attachment). Run on a CONSUMER instance AFTER the publisher deployed the
+// buildNote `image` fix:  cd ~/apps/<instance> && node scripts/backfill-cirkel-covers.mjs
+
+import 'dotenv/config';
+import db from '../src/config/database.js';
+
+function hasImg(mj) {
+  try { return JSON.parse(mj || '[]').some((m) => m.url && (!m.type || /image/i.test(m.type))); } catch { return false; }
+}
+async function fetchNote(url) {
+  try { const r = await fetch(url, { headers: { Accept: 'application/activity+json' } }); if (r.ok) return await r.json(); } catch { /* skip */ }
+  return null;
+}
+function coverFrom(note) {
+  if (!note) return null;
+  const atts = (Array.isArray(note.attachment) ? note.attachment : []).filter((a) => a && a.url && (/image/i.test(a.mediaType || '') || !a.mediaType));
+  if (atts.length) return { url: atts[0].url, type: atts[0].mediaType || 'image/jpeg' };
+  const im = Array.isArray(note.image) ? note.image[0] : note.image;
+  const iu = im && (typeof im === 'string' ? im : im.url);
+  if (iu) return { url: iu, type: (im && im.mediaType) || 'image/jpeg' };
+  return null;
+}
+
+let rows = [];
+try { rows = db.prepare('SELECT id, media_json FROM ap_timeline').all().filter((r) => !hasImg(r.media_json)); } catch { console.log('no ap_timeline'); process.exit(0); }
+let fixed = 0;
+for (const r of rows) {
+  const cov = coverFrom(await fetchNote(r.id));
+  if (!cov) continue;
+  let arr = []; try { arr = JSON.parse(r.media_json || '[]'); } catch { /* reset */ }
+  arr.push(cov);
+  try { db.prepare('UPDATE ap_timeline SET media_json = ? WHERE id = ?').run(JSON.stringify(arr), r.id); fixed++; } catch { /* skip */ }
+}
+console.log(`backfilled ${fixed}/${rows.length} coverless timeline posts`);
+process.exit(0);
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 5ceed6139eeea577f733bcbb9d3bd6ea49dda290)
+++ src/services/ActivityPubService.js	(revision c628db10ab7264c2115f59bdd80eefa38f5d7983)
@@ -241,4 +241,11 @@
   };
   if (attachment.length) note.attachment = attachment;
+  // Playable-audio posts suppress the cover attachment (player card). Still expose
+  // the cover via AS2 `image` so card/grid consumers (the Klonkt Cirkel) can show
+  // it — Mastodon ignores a Note's `image`, so the player card is unaffected.
+  if (post.cover_image_url && playable) {
+    const cov = abs(post.cover_image_url);
+    if (cov) note.image = { type: 'Image', mediaType: mediaType(cov), url: cov };
+  }
   return note;
 }
@@ -646,5 +653,13 @@
         const ai = actorInfo(await resolveActor(actorUri), actorUri);
         const html = HtmlSanitizerService.sanitize(o.content || '');
-        const media = JSON.stringify((Array.isArray(o.attachment) ? o.attachment : []).map((a) => ({ url: safeUrl(a && a.url), type: (a && a.mediaType) || '' })).filter((m) => m.url));
+        const _atts = (Array.isArray(o.attachment) ? o.attachment : []).map((a) => ({ url: safeUrl(a && a.url), type: (a && a.mediaType) || '' })).filter((m) => m.url);
+        // Fallback cover: a Note's `image` (set when the attachment was suppressed
+        // for a player-card post, e.g. hosted-audio posts).
+        if (!_atts.some((m) => !m.type || /image/i.test(m.type)) && o.image) {
+          const _im = Array.isArray(o.image) ? o.image[0] : o.image;
+          const _iu = safeUrl(typeof _im === 'string' ? _im : (_im && _im.url));
+          if (_iu) _atts.push({ url: _iu, type: (_im && _im.mediaType) || 'image/jpeg' });
+        }
+        const media = JSON.stringify(_atts);
         for (const s of subs) {
           tlStmts().ins.run(o.id, s.slug, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.url || null, o.published || null, media);
