Persist replacements across refresh (device-local, no account)
A reader who swaps a story away should keep that swap after a refresh; before, the server re-served the original brief. - localStorage now persists seen / dismissed / history (loadJSON/saveJSON). - /api/brief accepts an exclude list; dismissed (replaced-away) ids are dropped and the highlights refill around them, so swaps stick and stay full. - Replace records the swap to dismissed+seen and persists; the seen-set (persisted) keeps Replace from recycling across refreshes too. - History panel survives refresh and gains 'Clear what I've seen (start fresh)' so it never feels suffocating. Saved history/favorites still come with sign-in. Tests: brief exclude + refill (90 total). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+19
-15
@@ -313,28 +313,32 @@ def create_app() -> FastAPI:
|
||||
date: str | None = Query(None),
|
||||
limit: int = Query(10, ge=1, le=50),
|
||||
prefs: str | None = Query(None),
|
||||
exclude: str = Query("", description="comma-separated article ids the reader has dismissed"),
|
||||
) -> BriefResponse:
|
||||
fp = prefs_from_json(prefs)
|
||||
now = datetime.now(timezone.utc)
|
||||
excl = {int(x) for x in exclude.split(",") if x.strip().lstrip("-").isdigit()}
|
||||
with get_conn() as conn:
|
||||
data = queries.brief(conn, brief_date=date, limit=limit)
|
||||
items = data["items"]
|
||||
# Drop dismissed (replaced-away) items and anything the reader's
|
||||
# boundaries hide; avoid-terms take precedence over curation.
|
||||
items = [a for a in data["items"] if a["id"] not in excl]
|
||||
if not fp.is_empty():
|
||||
# Apply personal boundaries (avoid-terms take precedence over curation).
|
||||
items = filter_articles(items, fp, now)
|
||||
# Keep the highlights full: if a boundary hid a story, top up with
|
||||
# other readable, boundary-respecting good news rather than show fewer.
|
||||
if len(items) < limit:
|
||||
have = {a["id"] for a in items}
|
||||
pool = queries.feed(
|
||||
conn, accepted_only=True, limit=limit * 5 + 20, offset=0, **_prefs_sql_kw(fp, now)
|
||||
)
|
||||
for a in filter_articles(pool, fp, now):
|
||||
if len(items) >= limit:
|
||||
break
|
||||
if a["id"] not in have:
|
||||
items.append(a)
|
||||
have.add(a["id"])
|
||||
# Keep the highlights full: if a boundary or a dismissal removed a
|
||||
# story, top up with other readable, boundary-respecting good news
|
||||
# rather than show fewer.
|
||||
if len(items) < limit:
|
||||
have = {a["id"] for a in items} | excl
|
||||
pool = queries.feed(
|
||||
conn, accepted_only=True, limit=limit * 5 + 40, offset=0, **_prefs_sql_kw(fp, now)
|
||||
)
|
||||
for a in filter_articles(pool, fp, now):
|
||||
if len(items) >= limit:
|
||||
break
|
||||
if a["id"] not in have:
|
||||
items.append(a)
|
||||
have.add(a["id"])
|
||||
# Lead with a gentle, readable story (charged or paywalled stories stay
|
||||
# in the set, just not as the first thing seen).
|
||||
items = _pick_lead(items)
|
||||
|
||||
Reference in New Issue
Block a user