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:
@@ -69,12 +69,20 @@ class FilterPrefs:
|
||||
mute_flavors: list[str] = field(default_factory=list)
|
||||
avoid_terms: list[str] = field(default_factory=list)
|
||||
pauses: list[Pause] = field(default_factory=list)
|
||||
max_cortisol: int | None = None
|
||||
max_ragebait: int | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict | None) -> "FilterPrefs":
|
||||
if not isinstance(data, dict):
|
||||
return cls()
|
||||
|
||||
def _opt_int(value: object) -> int | None:
|
||||
try:
|
||||
return int(value) if value is not None else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
def _str_list(value: object) -> list[str]:
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
@@ -96,6 +104,8 @@ class FilterPrefs:
|
||||
mute_flavors=_str_list(data.get("mute_flavors")),
|
||||
avoid_terms=_str_list(data.get("avoid_terms")),
|
||||
pauses=pauses,
|
||||
max_cortisol=_opt_int(data.get("max_cortisol")),
|
||||
max_ragebait=_opt_int(data.get("max_ragebait")),
|
||||
)
|
||||
|
||||
def muted_topics(self, now: datetime) -> set[str]:
|
||||
@@ -117,6 +127,8 @@ class FilterPrefs:
|
||||
or self.mute_flavors
|
||||
or self.avoid_terms
|
||||
or self.pauses
|
||||
or self.max_cortisol is not None
|
||||
or self.max_ragebait is not None
|
||||
)
|
||||
|
||||
|
||||
@@ -148,6 +160,10 @@ def allows(article: dict, prefs: FilterPrefs, now: datetime) -> bool:
|
||||
return False
|
||||
if flavor in prefs.muted_flavors(now):
|
||||
return False
|
||||
if prefs.max_cortisol is not None and (article.get("cortisol_score") or 0) > prefs.max_cortisol:
|
||||
return False
|
||||
if prefs.max_ragebait is not None and (article.get("ragebait_score") or 0) > prefs.max_ragebait:
|
||||
return False
|
||||
blob = f"{article.get('title') or ''} {article.get('description') or ''}"
|
||||
if text_matches_avoid_terms(blob, prefs.avoid_terms):
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user