Per Codex — make /a/<id> feel like Upbeat Bytes has editorial judgment, not just
a summary wrapper. Trust-building, short, not an essay.
* article_summaries gains what_happened / why_matters / why_belongs (+ migration).
* summarize.explain_article: a separate, fallback-able LLM pass producing three
short notes (parsed from a labelled WHAT/MATTERS/BELONGS format). generate_summary
now stores them alongside the summary, and tops up older summaries on next view.
get_explanation returns them only when all three are present.
* API: share_page + /api/summary expose the explanation.
* share.py: renders the three-part section (accent rule) when complete; otherwise
the single "Why it's here" reason line is the calm fallback. The page polls and
swaps in both the summary and the section as they cache.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The /a/<id> page now carries an original short summary so it stands on its own,
without republishing the publisher's article:
- summarize.py: transient SSRF-guarded fetch of the article text → local LLM
writes a 2-4 sentence ORIGINAL summary (our words). Cached in article_summaries
forever; we store only our summary, never the body. Generated lazily (only for
shared/viewed articles), de-duped so concurrent hits don't double-generate.
- /a serves cached-or-pending; when pending it shows a calm "summary on its way,
read at {source}" note and self-polls /api/summary/<id>, swapping the summary
in the moment it's ready (never blocks the page on the batch-tier LLM).
- Share menu warms generation on open so recipients usually get the rich version.
- Container reaches the arbiter at arbiter:8080 over caddy_web (LLM env added to
the API container). 124 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>