Fix stale pinned-brief images; enrich all 7 + retry failures
Root cause (Codex audit): the client pins the brief by generated_at, but image
enrichment populates image_url AFTER the brief is built without bumping
generated_at — so a verbatim pinned copy stays imageless even once the server
has the image. The reclassify rebuilt the brief and the early pin stuck.
- Frontend: when reusing a pinned brief (same generated_at), refresh server-owned
metadata by article id (esp. image_url) while preserving the user's order and
replacements. Re-saves the merged view so it stays current.
- enrich_brief_images: default limit 5 -> 7 (any brief item can become the hero
via the client fallback or a replace, so cover the whole brief).
- Don't cache image failures forever: retry brief items still missing an image
after a TTL (retry_days=2) instead of stamping them imageless permanently.
Pairs with the hero image fallback (dd0087b). 99 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -119,7 +119,15 @@
|
||||
const sameServerBrief =
|
||||
view && view.generated_at && fetched.generated_at && view.generated_at === fetched.generated_at;
|
||||
if (!fresh && sameServerBrief && Array.isArray(view.items) && view.items.length) {
|
||||
brief = { ...fetched, items: view.items };
|
||||
// Same server brief: keep the user's pinned order + replacements, but
|
||||
// refresh server-owned metadata by id. image_url especially is enriched
|
||||
// AFTER the brief is built (without bumping generated_at), so a verbatim
|
||||
// pinned copy can stay imageless forever. Items the user swapped in
|
||||
// (absent from the fresh brief) keep their own data.
|
||||
const freshById = new Map(fetched.items.map((a) => [a.id, a]));
|
||||
const items = view.items.map((it) => freshById.get(it.id) ?? it);
|
||||
brief = { ...fetched, items };
|
||||
P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items });
|
||||
} else {
|
||||
brief = fetched;
|
||||
P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items: fetched.items });
|
||||
|
||||
+18
-9
@@ -146,27 +146,36 @@ def fetch_og_image(url: str | None) -> str | None:
|
||||
return None # too many redirects
|
||||
|
||||
|
||||
def enrich_brief_images(conn: sqlite3.Connection, brief_date: str, fetch=fetch_og_image, limit: int = 5) -> int:
|
||||
"""Fetch a hero-quality image for brief items that lack one (once each).
|
||||
def enrich_brief_images(
|
||||
conn: sqlite3.Connection, brief_date: str, fetch=fetch_og_image, limit: int = 7, retry_days: int = 2
|
||||
) -> int:
|
||||
"""Fetch a hero-quality image for brief items that lack one.
|
||||
|
||||
Only touches the given date's brief items with no image and not yet checked;
|
||||
stamps image_checked_at either way so failures are not retried forever.
|
||||
Any of the brief's items can become the hero (via the client's fallback or a
|
||||
replace), so this covers the whole brief (limit defaults to the brief size, 7),
|
||||
not just the top few. Items already carrying an image are left alone; items
|
||||
still without one are retried after `retry_days` so a transient fetch failure
|
||||
or a weaker earlier extractor doesn't mark an article imageless forever.
|
||||
Returns how many images were newly found.
|
||||
"""
|
||||
# Brief items not yet checked. We fetch even when a feed image exists,
|
||||
# because feed thumbnails are often tiny and the hero is shown large — a
|
||||
# page's og:image (the social-share image) is the better hero visual.
|
||||
# Fetch even when a feed image exists, because feed thumbnails are often tiny
|
||||
# and the hero is shown large — a page's og:image is the better hero visual.
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT a.id, a.canonical_url
|
||||
FROM daily_briefs b
|
||||
JOIN daily_brief_items bi ON bi.brief_id = b.id
|
||||
JOIN articles a ON a.id = bi.article_id
|
||||
WHERE b.brief_date = ? AND a.image_checked_at IS NULL
|
||||
WHERE b.brief_date = ?
|
||||
AND (
|
||||
a.image_checked_at IS NULL
|
||||
OR ((a.image_url IS NULL OR a.image_url = '')
|
||||
AND a.image_checked_at < datetime('now', ?))
|
||||
)
|
||||
ORDER BY bi.rank
|
||||
LIMIT ?
|
||||
""",
|
||||
(brief_date, limit),
|
||||
(brief_date, f"-{retry_days} days", limit),
|
||||
).fetchall()
|
||||
|
||||
found = 0
|
||||
|
||||
Reference in New Issue
Block a user