news-digest: real article summaries + per-desk collapse

Two upgrades to make the digest actually readable:

1) Article-grounded 2-3 sentence summaries (everywhere)

   The old prompt got just the title + miniflux's content excerpt,
   which for HN/Lobsters/wire feeds is barely more than the title
   itself — so summaries paraphrased the title and added nothing.

   Now every URL gets fetched and main-content-extracted via
   trafilatura on a parallel pre-pass (10 workers, ~15s for ~50
   URLs). Extracted text caches to /output/.article-cache.json with
   a 7-day TTL so repeat runs in the same window don't re-pull.

   Headlines also get summarized now — one batched LLM call per
   category (world / local). Rendered as a paragraph below the
   title with source + time on the right rail.

   Prompt rewrites tell the model to pull names/numbers/places
   from the body and explicitly forbid restating the title.
   Result: real specifics ("71% saw no pay increase globally",
   "third time in less than two weeks", "Islamabad and Moscow
   intermediaries") instead of title paraphrase.

2) Per-desk collapse buttons

   Chevron next to .desk-count toggles a .is-collapsed class.
   Collapsed state is per-device (localStorage by section id) since
   collapse is a viewing preference, not content state.
This commit is contained in:
vh
2026-04-28 11:29:15 -07:00
parent 018122648f
commit 40f1e0ee00
5 changed files with 325 additions and 45 deletions
+32
View File
@@ -201,6 +201,38 @@
});
}
/* ── desk collapse (per-device, localStorage) ───────────────────── */
// Stored as a JSON object {deskId: true} of collapsed desks. Per
// device by design — collapse is a viewing preference, not content
// state, so no server roundtrip.
const COLLAPSE_KEY = "digest:collapsed-desks";
function readCollapsed() {
try { return JSON.parse(localStorage.getItem(COLLAPSE_KEY) || "{}"); }
catch (_) { return {}; }
}
function writeCollapsed(obj) {
try { localStorage.setItem(COLLAPSE_KEY, JSON.stringify(obj)); } catch (_) {}
}
function applyCollapseState() {
const state = readCollapsed();
document.querySelectorAll(".desk[id]").forEach((desk) => {
desk.classList.toggle("is-collapsed", !!state[desk.id]);
});
}
applyCollapseState();
document.addEventListener("click", (ev) => {
const btn = ev.target.closest(".desk-collapse");
if (!btn) return;
const desk = btn.closest(".desk[id]");
if (!desk) return;
ev.preventDefault();
desk.classList.toggle("is-collapsed");
const state = readCollapsed();
if (desk.classList.contains("is-collapsed")) state[desk.id] = true;
else delete state[desk.id];
writeCollapsed(state);
});
/* ── initial paint ──────────────────────────────────────────────── */
apiGetHidden().then((hidden) => {