Files
thejayman77 5601022cf7 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>
2026-05-30 22:27:46 +00:00

71 lines
2.4 KiB
Python

import pytest
from goodnews import queries
from goodnews.db import connect, init_db
@pytest.fixture
def conn():
c = connect(":memory:")
init_db(c)
c.execute("INSERT INTO sources (id, name, feed_url, trust_score) VALUES (1,'S','http://s/f',5)")
rows = [
(1, "science", "discovery", 1),
(2, "animals", "discovery", 1),
(3, "health", "breakthrough", 2),
(4, "community", "solution", 1),
(5, "environment", "solution", 7), # high cortisol
]
for aid, topic, flavor, cort in rows:
c.execute(
"INSERT INTO articles (id, source_id, canonical_url, title, url_hash) VALUES (?,1,?,?,?)",
(aid, f"http://s/{aid}", f"t{aid}", f"h{aid}"),
)
c.execute(
"INSERT INTO article_scores (article_id, constructive_score, agency_score, human_benefit_score, "
"cortisol_score, ragebait_score, pr_risk_score, accepted, topic, flavor) "
"VALUES (?, 6, 2, 2, ?, 0, 2, 1, ?, ?)",
(aid, cort, topic, flavor),
)
c.commit()
yield c
c.close()
def _topics(rows):
return sorted({r["topic"] for r in rows})
def test_include_topics_in_sql(conn):
rows = queries.feed(conn, include_topics=["science", "animals"], limit=50)
assert _topics(rows) == ["animals", "science"]
def test_include_flavors_in_sql(conn):
rows = queries.feed(conn, include_flavors=["discovery"], limit=50)
assert sorted({r["flavor"] for r in rows}) == ["discovery"]
def test_include_topic_and_flavor_are_anded(conn):
# Wonder-style: (science/animals/culture) AND discovery
rows = queries.feed(conn, include_topics=["science", "animals", "culture"], include_flavors=["discovery"], limit=50)
assert _topics(rows) == ["animals", "science"] # the two discovery items
def test_mute_topics_excludes(conn):
rows = queries.feed(conn, mute_topics=["health", "environment"], limit=50)
assert "health" not in _topics(rows) and "environment" not in _topics(rows)
def test_max_cortisol_ceiling(conn):
rows = queries.feed(conn, max_cortisol=2, limit=50)
assert all((r["cortisol_score"] or 0) <= 2 for r in rows)
assert "environment" not in _topics(rows) # the cortisol=7 item is gone
def test_duplicates_excluded(conn):
conn.execute("UPDATE articles SET duplicate_of = 1 WHERE id = 2")
conn.commit()
ids = {r["id"] for r in queries.feed(conn, limit=50)}
assert 2 not in ids