Summary briefing layer: Today pre-summarized, /a is the canonical read

Make summaries the core reading experience (summary-first, source-forward):
- Cycle pre-warms summaries for Today's 7 (idempotent → only new ones hit the LLM).
- /api/brief items carry their cached summary; Today cards (hero + tiles) show it
  inline, so Today reads as a calm briefing.
- Card title/image now open the /a summary page (the canonical artifact), with a
  visible "Full story" link straight to the source on every card (the escape hatch).
- /a gains related-grouping chips + a Copy-link/share control.
- Tighten the summary prompt: original, factual, no quotations / no close paraphrase.
Long tail stays lazy+cached. No article bodies stored. 129 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-04 19:48:32 +00:00
parent 3924d927aa
commit cfde4e22db
6 changed files with 80 additions and 14 deletions
+4 -1
View File
@@ -243,11 +243,13 @@ class Article(BaseModel):
rank: int | None = None # position within a brief, when applicable
paywalled: bool = False
tags: list[str] = []
summary: str | None = None # our own cached summary (present on the brief)
@classmethod
def from_row(cls, row: dict) -> "Article":
raw_tags = row.get("tags")
return cls(
summary=row.get("summary"),
id=row["id"],
title=row["title"],
description=row.get("description"),
@@ -693,7 +695,8 @@ def create_app() -> FastAPI:
with get_conn() as conn:
row = conn.execute(
"SELECT a.id, a.title, a.description, a.image_url, a.canonical_url, "
"a.duplicate_of, src.name AS source_name, s.reason_text, s.accepted "
"a.duplicate_of, src.name AS source_name, s.reason_text, s.accepted, "
"(SELECT group_concat(t.tag) FROM article_tags t WHERE t.article_id = a.id) AS tags "
"FROM articles a JOIN sources src ON src.id = a.source_id "
"LEFT JOIN article_scores s ON s.article_id = a.id WHERE a.id = ?",
(aid,),
+19
View File
@@ -10,6 +10,7 @@ from .briefs import build_daily_brief, show_brief
from .db import connect, init_db
from .dedup import DEFAULT_THRESHOLD, DEFAULT_WINDOW_DAYS, dedup as run_dedup
from .enrich import enrich_brief_images
from .summarize import generate_summary, get_summary
from .feeds import (
fetch_feed,
parse_feed,
@@ -452,6 +453,24 @@ def _run_cycle_locked(conn: sqlite3.Connection, args: argparse.Namespace) -> Non
except Exception as exc:
print(f"brief: skipped ({exc})")
# Pre-warm summaries for today's brief so Today reads as a calm briefing.
# Idempotent: cached items are skipped, so this only hits the LLM for new ones.
try:
client = llm_client_from_args(args)
ids = [r[0] for r in conn.execute(
"SELECT bi.article_id FROM daily_briefs b JOIN daily_brief_items bi "
"ON bi.brief_id = b.id WHERE b.brief_date = ?", (today,)
)]
made = 0
for aid in ids:
had = get_summary(conn, aid)
generate_summary(conn, aid, client=client)
if not had and get_summary(conn, aid):
made += 1
print(f"summaries: {made} new (of {len(ids)} brief items)")
except Exception as exc:
print(f"summaries: skipped ({exc})")
if not args.no_review:
try:
flagged = review_sources(conn)
+2 -1
View File
@@ -133,7 +133,8 @@ def brief(conn: sqlite3.Connection, brief_date: str | None = None, limit: int =
rows = conn.execute(
f"""
SELECT bi.rank, bi.selection_reason, {_ARTICLE_COLUMNS}
SELECT bi.rank, bi.selection_reason, {_ARTICLE_COLUMNS},
(SELECT summary FROM article_summaries WHERE article_id = a.id) AS summary
FROM daily_briefs b
JOIN daily_brief_items bi ON bi.brief_id = b.id
JOIN articles a ON a.id = bi.article_id
+25 -2
View File
@@ -49,6 +49,13 @@ def render_share_page(article: dict, base_url: str, summary: str | None = None)
if image else ""
)
raw_tags = (article.get("tags") or "")
chips = "".join(
f'<span class="chip">{escape(t.replace("-", " "))}</span>'
for t in raw_tags.split(",") if t
)
groupings = f'<div class="chips">{chips}</div>' if chips else ""
source_short = source.split(" ")[0] if source else "the source"
if summary:
summary_block = f'<p class="summary" id="summary">{escape(summary)}</p>'
@@ -122,11 +129,15 @@ def render_share_page(article: dict, base_url: str, summary: str | None = None)
.why {{ color:#3b4754; font-size:.92rem; margin:0 0 22px; }}
.why .lbl {{ display:block; text-transform:uppercase; letter-spacing:.08em; font-size:.7rem;
color:var(--muted); margin-bottom:3px; }}
.chips {{ display:flex; flex-wrap:wrap; gap:7px; margin:0 0 22px; }}
.chip {{ background:#e0eef3; color:var(--accent-deep); border-radius:999px; padding:3px 11px;
font-size:.7rem; text-transform:uppercase; letter-spacing:.06em; }}
.actions {{ display:flex; flex-wrap:wrap; gap:12px; align-items:center; }}
.primary {{ background:var(--accent); color:#fff; text-decoration:none; font-weight:600;
padding:12px 20px; border-radius:999px; display:inline-block; }}
.primary:hover {{ background:var(--accent-deep); }}
.secondary {{ color:var(--accent-deep); text-decoration:none; font-size:.92rem; }}
.secondary {{ color:var(--accent-deep); text-decoration:none; font-size:.92rem;
background:none; border:none; cursor:pointer; font-family:inherit; padding:0; }}
.secondary:hover {{ text-decoration:underline; }}
.note {{ color:var(--muted); font-size:.8rem; margin-top:22px; }}
</style>
@@ -141,15 +152,27 @@ def render_share_page(article: dict, base_url: str, summary: str | None = None)
<h1>{escape(title)}</h1>
{summary_block}
<p class="why"><span class="lbl">Why it's here</span>{escape(why)}</p>
{groupings}
<div class="actions">
<a class="primary" href="{escape(src_url)}" rel="noopener" data-src-click>Read the full story at {escape(source_short)}</a>
<a class="secondary" href="/">Explore more on Upbeat Bytes →</a>
<button class="secondary" type="button" data-share>Copy link</button>
<a class="secondary" href="/">Explore Upbeat Bytes →</a>
</div>
<p class="note">Upbeat Bytes summarizes in its own words and links to the original publisher — it doesn't host the article.</p>
</div>
</article>
</main>
{src_click}
<script>
(function(){{
var s=document.querySelector('[data-share]'); if(!s) return;
s.addEventListener('click',function(){{
var url=location.href;
if(navigator.share){{ navigator.share({{title:document.title,url:url}}).catch(function(){{}}); }}
else if(navigator.clipboard){{ navigator.clipboard.writeText(url).then(function(){{ s.textContent='Copied!'; setTimeout(function(){{s.textContent='Copy link';}},1500); }}); }}
}});
}})();
</script>
{poll}
</body>
</html>"""
+5 -4
View File
@@ -26,10 +26,11 @@ _TAGS = re.compile(rb"<[^>]+>")
_WS = re.compile(r"\s+")
_SYSTEM = (
"You write a short, original summary of a news story for a calm, constructive news "
"site. Use your own words — never copy sentences from the source. 2 to 4 plain "
"sentences: what happened and why it's encouraging. No preamble, no markdown, no "
"headline — just the summary."
"You write a short, ORIGINAL summary of a news story for a calm, constructive news "
"site. Summarize the underlying FACTS in your own words. Do NOT quote, and do not "
"closely paraphrase the article's sentences — no lifted phrases. 2 to 4 plain "
"sentences covering what happened and why it's encouraging. No preamble, no markdown, "
"no headline, no opinion — just the factual summary."
)