Local-first Brief: the landing leads with good news from your home

Per the owner's call (overrides the earlier "Brief sacred" stance): when a home is
set, the homepage opens with local good news first, not global. This is the hook —
you land and see awesome stories from YOUR corner first.

- queries.home_brief: local-first highlights (high/medium-confidence near, blended
  out to country then world so it's always a full, strong set), preferring already-
  summarized stories so the calm read stays rich. Recent window, ranked within tier.
- /api/brief gains a `home` param: private/no-store when set; over-fetches + caps so
  dismissal/boundary filtering never thins it; falls back to global top-up if needed.
- Landing UI: a Local <-> Global toggle ("📍 Near you / 🌍 Everywhere") when a home
  is set, the calm picker invite when not (dismissible), and Change. Default leads
  local; one tap back to the global brief. No home set => exactly today's behavior.

Backend + frontend tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-19 21:36:18 -04:00
parent 2239549799
commit d2a6293a13
7 changed files with 1784 additions and 16 deletions
+22 -5
View File
@@ -2186,25 +2186,42 @@ def create_app() -> FastAPI:
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"),
home: str | None = Query(None, max_length=8, description="local-first highlights: 'US' or 'US-NY'"),
) -> BriefResponse:
# The default highlights are global (date-keyed, no session) → edge-cacheable
# so a new visitor's "Gathering the good news…" resolves from their POP, not
# a pull to the residential origin. Personal filters stay private.
shareable = not prefs and not exclude.strip()
# a pull to the residential origin. Personal filters (incl. a home) stay private.
home_country = home_state = None
if home:
parts = home.upper().split("-", 1)
home_country = parts[0][:2] or None
if home_country == "US" and len(parts) > 1:
home_state = parts[1][:2] or None
shareable = not prefs and not exclude.strip() and not home_country
response.headers["Cache-Control"] = _EDGE_FEED if shareable else _PRIVATE
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)
if home_country:
# The reader's home leads the landing: local good news first, blended out
# to country/world so it's always a full, sexy set. Over-fetch to survive
# dismissal/boundary filtering, then cap to limit.
meta = queries.brief(conn, brief_date=date, limit=1)
data = {"brief_date": meta["brief_date"], "title": "Close to home", "created_at": meta.get("created_at")}
pool = queries.home_brief(conn, home_country, home_state, limit=limit + 12)
else:
data = queries.brief(conn, brief_date=date, limit=limit)
pool = 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]
items = [a for a in pool if a["id"] not in excl]
if not fp.is_empty():
items = filter_articles(items, fp, now)
items = items[:limit] # home mode over-fetches to survive filtering; cap here
# 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.
# rather than show fewer. (Home mode's home_brief already blends to world.)
if len(items) < limit:
have = {a["id"] for a in items} | excl
pool = queries.feed(