Build the SvelteKit frontend: calm home with mood modes
- New frontend/ SvelteKit static SPA (Svelte 5), served by FastAPI from frontend/build (falls back to the legacy page if unbuilt). - Calm design system: cream/sage palette, serif headlines, generous space, no urgency colors, gentle motion (respects prefers-reduced-motion). - Home screen: mood-mode nav (Today/Wonder/People Helping/Solutions/Light Only/Grounded), the daily brief as a hero + remaining four, browsable mood lanes, an explicit calm end-state, inline Not today / Less like this / Hide affordances, and device-local Calm Filters mirroring goodnews/filters.py. - Backend: moods.py + GET /api/moods (single source of truth for the modes); FilterPrefs gains max_cortisol/max_ragebait ceilings (for Light Only). - Push categorical filters (include/mute topics+flavors, ceilings) into SQL in queries.feed so low-ranked-but-matching items (e.g. discovery for Wonder) are not truncated by ranking; only avoid-terms stay a Python pass. - PWA manifest + icon (installable; offline deferred per plan). - Multi-stage Dockerfile builds the site then serves it from the API. - Tests: queries.feed categorical filters (63 total). README updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+35
-8
@@ -31,11 +31,15 @@ from . import feeds, queries
|
||||
from .db import connect, init_db
|
||||
from .filters import filter_articles, prefs_from_json
|
||||
from .llm import LocalModelClient
|
||||
from .moods import MOODS
|
||||
from .taxonomy import FLAVORS, TOPICS
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_DB = ROOT / "data" / "goodnews.sqlite3"
|
||||
STATIC_DIR = Path(__file__).resolve().parent / "static"
|
||||
# Prefer the built SvelteKit site; fall back to the legacy single-page harness.
|
||||
FRONTEND_DIR = ROOT / "frontend" / "build"
|
||||
LEGACY_STATIC = Path(__file__).resolve().parent / "static"
|
||||
STATIC_DIR = FRONTEND_DIR if FRONTEND_DIR.is_dir() else LEGACY_STATIC
|
||||
|
||||
|
||||
def db_path() -> Path:
|
||||
@@ -188,6 +192,12 @@ def create_app() -> FastAPI:
|
||||
flavors=[Category(key=k, description=v) for k, v in FLAVORS.items()],
|
||||
)
|
||||
|
||||
@app.get("/api/moods")
|
||||
def moods() -> list[dict]:
|
||||
# The humane front door: each mood resolves to a filter preset the
|
||||
# client merges with the user's own Calm Filters.
|
||||
return MOODS
|
||||
|
||||
@app.get("/api/category-counts", response_model=list[CategoryCount])
|
||||
def category_counts(accepted_only: bool = True, prefs: str | None = Query(None)) -> list[CategoryCount]:
|
||||
fp = prefs_from_json(prefs)
|
||||
@@ -220,20 +230,37 @@ def create_app() -> FastAPI:
|
||||
if flavor and flavor.lower() not in FLAVORS:
|
||||
raise HTTPException(400, f"unknown flavor: {flavor}")
|
||||
fp = prefs_from_json(prefs)
|
||||
now = datetime.now(timezone.utc)
|
||||
with get_conn() as conn:
|
||||
if fp.is_empty():
|
||||
rows = queries.feed(
|
||||
conn, topic=topic, flavor=flavor, accepted_only=accepted_only, limit=limit, offset=offset
|
||||
)
|
||||
else:
|
||||
# Over-fetch, apply the calm filters in Python (word-boundary
|
||||
# avoid-terms can't be done in SQL), then slice to the page.
|
||||
fetch_n = min(2000, (offset + limit) * 4 + 50)
|
||||
raw = queries.feed(
|
||||
conn, topic=topic, flavor=flavor, accepted_only=accepted_only, limit=fetch_n, offset=0
|
||||
# Categorical filters (include/mute topics+flavors incl. active
|
||||
# pauses, cortisol ceiling) go to SQL so nothing is truncated by
|
||||
# ranking. Only word-boundary avoid-terms need a Python pass, so
|
||||
# over-fetch just enough to cover what they might remove.
|
||||
kw = dict(
|
||||
include_topics=fp.include_topics or None,
|
||||
include_flavors=fp.include_flavors or None,
|
||||
mute_topics=list(fp.muted_topics(now)) or None,
|
||||
mute_flavors=list(fp.muted_flavors(now)) or None,
|
||||
max_cortisol=fp.max_cortisol,
|
||||
max_ragebait=fp.max_ragebait,
|
||||
)
|
||||
filtered = filter_articles(raw, fp, datetime.now(timezone.utc))
|
||||
rows = filtered[offset : offset + limit]
|
||||
if fp.avoid_terms:
|
||||
raw = queries.feed(
|
||||
conn, topic=topic, flavor=flavor, accepted_only=accepted_only,
|
||||
limit=min(2000, (offset + limit) * 4 + 50), offset=0, **kw,
|
||||
)
|
||||
kept = filter_articles(raw, fp, now) # drops avoid-term matches
|
||||
rows = kept[offset : offset + limit]
|
||||
else:
|
||||
rows = queries.feed(
|
||||
conn, topic=topic, flavor=flavor, accepted_only=accepted_only,
|
||||
limit=limit, offset=offset, **kw,
|
||||
)
|
||||
return FeedResponse(
|
||||
topic=topic,
|
||||
flavor=flavor,
|
||||
|
||||
Reference in New Issue
Block a user