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:
jay
2026-06-02 07:38:54 +00:00
parent dd0087b8b3
commit 6d5bcb13e5
2 changed files with 27 additions and 10 deletions
+18 -9
View File
@@ -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