Index: CHANGELOG.de.md
===================================================================
--- CHANGELOG.de.md	(revision 6dd1a3f0ef839a8ce97dec8cc96e5b00ae93a8bc)
+++ CHANGELOG.de.md	(revision 14f54a7a8347f9521ad9b1f426f36b48b00993d6)
@@ -9,7 +9,9 @@
 - **Boosts, die ihr Cover verloren hatten, bekommen es automatisch zurück.**
   Beiträge, die du vor dem Cover-Fix geboostet hast, waren ohne Artwork
-  gespeichert; sie werden beim nächsten Neustart einmalig aufgefrischt. Einen
-  Beitrag erneut zu boosten aktualisiert jetzt ebenfalls die gespeicherte Kopie
-  (Cover, Inhalt) — ein veralteter Boost lässt sich also auch von Hand heilen.
+  gespeichert; sie werden beim nächsten Neustart einmalig aufgefrischt. Ist der
+  Heimserver eines Beitrags in dem Moment kurz nicht erreichbar, wird es bei den
+  nächsten Neustarts erneut versucht statt endgültig übersprungen. Einen Beitrag
+  erneut zu boosten aktualisiert jetzt ebenfalls die gespeicherte Kopie (Cover,
+  Inhalt) — aus dem Feed wie von der Interact-Seite.
 
 ## [1.3.3] — 2026-07-03
Index: CHANGELOG.md
===================================================================
--- CHANGELOG.md	(revision 6dd1a3f0ef839a8ce97dec8cc96e5b00ae93a8bc)
+++ CHANGELOG.md	(revision 14f54a7a8347f9521ad9b1f426f36b48b00993d6)
@@ -9,6 +9,8 @@
 - **Boosts that lost their cover get it back automatically.** Posts you boosted
   before the cover fix were cached without their artwork; they are refreshed
-  once on the next restart. Boosting a post again now also refreshes its cached
-  copy (cover, content), so a stale boost can always be healed by hand too.
+  once on the next restart. If a post's home server is briefly unreachable at
+  that moment, it is retried on the next restarts instead of being skipped for
+  good. Boosting a post again now also refreshes its cached copy (cover,
+  content) — from the feed as well as the interact page.
 
 ## [1.3.3] — 2026-07-03
Index: CHANGELOG.nl.md
===================================================================
--- CHANGELOG.nl.md	(revision 6dd1a3f0ef839a8ce97dec8cc96e5b00ae93a8bc)
+++ CHANGELOG.nl.md	(revision 14f54a7a8347f9521ad9b1f426f36b48b00993d6)
@@ -9,7 +9,9 @@
 - **Boosts die hun cover kwijt waren krijgen 'm automatisch terug.** Posts die
   je vóór de cover-fix boostte waren zonder artwork opgeslagen; die worden bij
-  de volgende herstart eenmalig ververst. Een post opnieuw boosten ververst nu
-  ook de opgeslagen kopie (cover, inhoud), dus een oude boost is altijd ook
-  handmatig te genezen.
+  de volgende herstart eenmalig ververst. Is de thuis-server van een post op dat
+  moment even onbereikbaar, dan wordt het bij volgende herstarts opnieuw
+  geprobeerd in plaats van voorgoed overgeslagen. Een post opnieuw boosten
+  ververst nu ook de opgeslagen kopie (cover, inhoud) — vanuit de feed én de
+  interact-pagina.
 
 ## [1.3.3] — 2026-07-03
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 6dd1a3f0ef839a8ce97dec8cc96e5b00ae93a8bc)
+++ src/routes/posts.js	(revision 14f54a7a8347f9521ad9b1f426f36b48b00993d6)
@@ -867,5 +867,15 @@
     on = !ActivityPubService.getTimelineReaction(site.slug, note).boosted;
     try { await ActivityPubService.sendInteraction(site, on ? 'boost' : 'unboost', note, (req.body.author || '').toString()); } catch (e) { /* ignore */ }
-    if (on) ActivityPubService.markBoosted(site.slug, note); else ActivityPubService.unmarkBoosted(site.slug, note);
+    if (on) {
+      ActivityPubService.markBoosted(site.slug, note); // instant UI state
+      // Fire-and-forget: re-resolve the note so the cached row is refreshed
+      // (cover/content) — boosting again heals a stale copy from EVERY boost
+      // path, not just the interact page.
+      ActivityPubService.resolveRemoteNote(note)
+        .then((n) => { if (n) ActivityPubService.upsertBoostedNote(site.slug, n); })
+        .catch(() => { /* best-effort */ });
+    } else {
+      ActivityPubService.unmarkBoosted(site.slug, note);
+    }
   }
   if (req.get('X-Requested-With') === 'fetch') return res.json({ ok: true, on });
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 6dd1a3f0ef839a8ce97dec8cc96e5b00ae93a8bc)
+++ src/services/ActivityPubService.js	(revision 14f54a7a8347f9521ad9b1f426f36b48b00993d6)
@@ -1878,5 +1878,5 @@
 // during a flux window, e.g. a fleet-wide update), and drops notes that are gone
 // (404/410). Bump SELFHEAL_VERSION only on a release that warrants a re-sync.
-const SELFHEAL_VERSION = 6; // v6: re-fetch so boosted notes cached coverless (pre image-fallback in resolveRemoteNote) pick up their cover
+const SELFHEAL_VERSION = 7; // v7: rerun v6 with retry-until-clean semantics (origins briefly offline no longer stay stale forever)
 async function fetchNoteAP(url) {
   try {
@@ -2052,10 +2052,10 @@
     let rows = [];
     try { rows = db.prepare('SELECT id, content, media_json, nsfw, cw, url FROM ap_timeline ORDER BY rowid DESC LIMIT 200').all(); } catch { /* no table */ }
-    let healed = 0;
+    let healed = 0, failed = 0;
     for (const r of rows) {
       try {
         const note = await fetchNoteAP(r.id);
         if (note === 404) { db.prepare('DELETE FROM ap_timeline WHERE id = ?').run(r.id); healed++; continue; }
-        if (!note || typeof note !== 'object') continue;
+        if (!note || typeof note !== 'object') { failed++; continue; } // origin unreachable right now
         const html = HtmlSanitizerService.sanitize(note.content || '');
         const media = mediaFromNote(note);
@@ -2067,8 +2067,21 @@
           healed++;
         }
-      } catch { /* per-note best-effort */ }
-    }
-    try { db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)').run('selfheal_version', String(SELFHEAL_VERSION)); } catch { /* ignore */ }
-    if (rows.length) console.log(`[AP] self-heal v${SELFHEAL_VERSION}: ${healed}/${rows.length} timeline notes`);
+      } catch { failed++; /* per-note best-effort */ }
+    }
+    // Only mark this version DONE after a clean pass. Some origins are briefly
+    // offline exactly when we heal (phone-hosted instances!): skipping them and
+    // consuming the version would leave those rows stale forever. Instead retry
+    // on the next boots, giving up after a few attempts (permanently-dead
+    // origins answer 404/410 and are deleted above, so they don't loop).
+    const setSetting = (k, v) => { try { db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)').run(k, String(v)); } catch { /* ignore */ } };
+    let attempts = 0;
+    try { const a = db.prepare('SELECT value FROM app_settings WHERE key = ?').get('selfheal_attempts'); attempts = a ? (parseInt(a.value, 10) || 0) : 0; } catch { /* ignore */ }
+    if (failed === 0 || attempts >= 4) {
+      setSetting('selfheal_version', SELFHEAL_VERSION);
+      setSetting('selfheal_attempts', 0);
+    } else {
+      setSetting('selfheal_attempts', attempts + 1);
+    }
+    if (rows.length) console.log(`[AP] self-heal v${SELFHEAL_VERSION}: ${healed}/${rows.length} timeline notes${failed ? ` (${failed} unreachable — will retry next boot)` : ''}`);
   } catch { /* never block boot */ } finally { _selfHealing = false; }
 }
