Pin the curated brief across refresh (stable, not dynamic)

Persisting only 'dismissed' kept swapped-away stories out but let the brief
recompose on refresh — so a chosen replacement (and the hero) could change
unexpectedly. Now the reader's actual brief view is persisted per day:
- loadToday keeps the saved view for the same brief_date (swaps and hero hold
  steady); re-fetches fresh on a new day or when forced.
- A boundary change forces a fresh re-fetch (and re-pins); Replace pins the new
  view; Clear-session drops the pin so it re-composes fresh.

Frontend only — rebuild + refresh (no server restart needed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-05-31 13:51:00 +00:00
parent 3fe7c4f228
commit f599f9d28e
+25 -7
View File
@@ -23,6 +23,7 @@
const SEEN_KEY = 'goodnews:seen';
const DISMISSED_KEY = 'goodnews:dismissed';
const HISTORY_KEY = 'goodnews:history';
const BRIEF_VIEW_KEY = 'goodnews:brief_view'; // {date, items} — the reader's curated brief
const HISTORY_CAP = 200;
let seenIds = new Set();
@@ -53,8 +54,9 @@
dismissed = new Set();
history = [];
persistSession();
P.saveJSON(BRIEF_VIEW_KEY, null); // drop the pinned brief so it re-composes fresh
showHistory = false;
select(selected);
select(selected, true);
}
let filtersOn = $derived(P.active(userPrefs));
@@ -69,17 +71,31 @@
return P.param(merged);
}
async function select(key) {
async function loadToday(fresh) {
const q = P.param(userPrefs);
const ex = Array.from(dismissed).join(',');
const fetched = await getJSON(`/api/brief?limit=7${q ? '&' + q : ''}${ex ? '&exclude=' + ex : ''}`);
const view = P.loadJSON(BRIEF_VIEW_KEY, null);
// On a plain (re)load, keep the reader's curated view for the same day — so
// swaps and the hero hold steady. Re-fetch fresh on a new day's brief, or
// when forced (e.g. a boundary changed and must be re-applied).
if (!fresh && view && view.date === fetched.brief_date && Array.isArray(view.items) && view.items.length) {
brief = { brief_date: view.date, title: fetched.title, items: view.items };
} else {
brief = fetched;
P.saveJSON(BRIEF_VIEW_KEY, { date: fetched.brief_date, items: fetched.items });
}
remember(brief.items);
}
async function select(key, fresh = false) {
selected = key;
error = '';
try {
// Today = the day's highlights (hero + six). Other moods reveal that
// category only when chosen.
if (key === 'today') {
const q = P.param(userPrefs);
const ex = Array.from(dismissed).join(',');
brief = await getJSON(`/api/brief?limit=7${q ? '&' + q : ''}${ex ? '&exclude=' + ex : ''}`);
remember(brief.items);
await loadToday(fresh);
} else {
const mood = moods.find((m) => m.key === key);
const q = P.param(P.merge(userPrefs, mood?.filter ?? {}));
@@ -96,7 +112,7 @@
function refreshPrefs() {
userPrefs = { ...userPrefs };
P.save(userPrefs);
select(selected);
select(selected, true); // boundaries changed — re-fetch so they apply
}
function applyAction(kind, value) {
P[kind]?.(userPrefs, value);
@@ -139,6 +155,8 @@
if (i >= 0) {
brief.items[i] = repl;
brief = { ...brief, items: [...brief.items] };
// Pin the swap so this exact curated brief survives a refresh.
P.saveJSON(BRIEF_VIEW_KEY, { date: brief.brief_date, items: brief.items });
}
} else {
const i = feed.findIndex((a) => a.id === article.id);