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:
+38
-2
@@ -47,8 +47,20 @@ def feed(
|
||||
accepted_only: bool = True,
|
||||
limit: int = 30,
|
||||
offset: int = 0,
|
||||
include_topics: list[str] | None = None,
|
||||
include_flavors: list[str] | None = None,
|
||||
mute_topics: list[str] | None = None,
|
||||
mute_flavors: list[str] | None = None,
|
||||
max_cortisol: int | None = None,
|
||||
max_ragebait: int | None = None,
|
||||
) -> list[dict]:
|
||||
"""Return ranked articles, optionally filtered by topic and/or flavor."""
|
||||
"""Return ranked articles with categorical filters applied in SQL.
|
||||
|
||||
Categorical filters (topic/flavor include & mute, cortisol/ragebait ceilings)
|
||||
must be applied here, not after ranking — otherwise low-ranked-but-matching
|
||||
items (e.g. 'discovery' for a Wonder lane) fall outside any over-fetch window.
|
||||
Word-boundary avoid-terms remain a Python pass on the caller side.
|
||||
"""
|
||||
clauses = ["a.duplicate_of IS NULL"]
|
||||
params: list = []
|
||||
if accepted_only:
|
||||
@@ -59,7 +71,31 @@ def feed(
|
||||
if flavor:
|
||||
clauses.append("s.flavor = ?")
|
||||
params.append(flavor.lower())
|
||||
where = ("WHERE " + " AND ".join(clauses)) if clauses else ""
|
||||
|
||||
def _in(column: str, values: list[str], negate: bool = False) -> None:
|
||||
vals = [v.lower() for v in values]
|
||||
placeholders = ",".join("?" * len(vals))
|
||||
op = "NOT IN" if negate else "IN"
|
||||
# COALESCE keeps NULL-category rows from being dropped by NOT IN.
|
||||
clauses.append(f"COALESCE({column}, '') {op} ({placeholders})")
|
||||
params.extend(vals)
|
||||
|
||||
if include_topics:
|
||||
_in("s.topic", include_topics)
|
||||
if include_flavors:
|
||||
_in("s.flavor", include_flavors)
|
||||
if mute_topics:
|
||||
_in("s.topic", mute_topics, negate=True)
|
||||
if mute_flavors:
|
||||
_in("s.flavor", mute_flavors, negate=True)
|
||||
if max_cortisol is not None:
|
||||
clauses.append("COALESCE(s.cortisol_score, 0) <= ?")
|
||||
params.append(max_cortisol)
|
||||
if max_ragebait is not None:
|
||||
clauses.append("COALESCE(s.ragebait_score, 0) <= ?")
|
||||
params.append(max_ragebait)
|
||||
|
||||
where = "WHERE " + " AND ".join(clauses)
|
||||
params.extend([limit, offset])
|
||||
|
||||
rows = conn.execute(
|
||||
|
||||
Reference in New Issue
Block a user