- ArticleCard: derive safeHref from article.url and reset image-failure state
when the article changes, so in-place replacements re-evaluate correctly
(clears the Svelte capture warning; build is warning-free again).
- Downweight paywalled stories below readable ones (stable sort) when composing
the daily five and in feed results — the brief now leads readable and rarely
hands over a locked door.
- review_sources gains a 'paywall-heavy' advisory flag (Nature, New Scientist
flag at 100%); never auto-deactivates.
- New Scientist/Nature kept active but no longer reach the daily five; they
remain browsable with the label + Replace.
- Tests: brief readability preference + paywall-heavy flag (79 total).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- paywall.py: conservative domain-level paywall detection (New Scientist,
Nature, and common hard/soft paywalls). Never fetches pages — an honest hint.
- API: Article gains a 'paywalled' flag; the brief now leads with a gentle AND
readable story (paywalled/charged stories stay in the five, just not first).
- New GET /api/replacement returns the next-best readable, unshown article
(honors mood+prefs via the merged prefs param; gentle=true for hero swaps).
- UI: paywalled cards show 'May need a subscription'; a Replace / 'Find one I
can read' action (always visible, while tuning actions stay tucked) swaps the
card for a readable alternative, with a gentle notice when none remain.
- Tests: paywall detection + replacement behavior (77 total).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Text-first lane cards were rendering the full source description, ballooning
them to several times the height of image cards.
- Show the source description only on the hero; lane/grid cards show title + the
one-sentence 'why', which is a tidy uniform summary.
- Line-clamp lane card titles and 'why' to 3 lines (hero description to 6) so
variable text length can't blow up a card. Rows now stay even.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per the calm north star (images support reading, never become a stimulation
layer; metadata-only stays the posture):
- Image-less cards are now designed, not missing: secondary cards are text-first
(no empty media band), and an image-less hero becomes a fully typographic lead
with a faint topic wordmark behind it (CSS attr(data-topic)). No big empty
image space is ever reserved.
- Opportunistic extraction: parse the first <img src> from a feed's
content/description HTML when present, canonicalized — never fetching the
article page. Applies to new ingests (existing rows keep their current image).
- Held by deliberate choice: og:image page enrichment, stock/AI imagery, and any
image-coverage requirement for sources.
Tests: feed HTML image extraction (72 total).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hero guardrail (core to the promise, not cosmetic):
- New hero.py: the lead story is chosen with a stricter filter than the rest of
the brief — very low cortisol/ragebait and no grief/medical/violence terms
(cancer, glioblastoma, death, diagnosis, ...). Such constructive-but-charged
stories stay among the five; they just never lead by default.
- /api/brief applies user avoid-terms FIRST, then lead_with_gentle, so personal
boundaries always take precedence over the general guardrail.
- Verified live: the brief no longer leads with a glioblastoma story.
Card polish (per review):
- Secondary cards with no real image are now text-first (no row of empty media
bands); hero still always shows media or a typographic fallback.
- Inline tuning actions are quiet until hover/focus on pointer devices, and stay
visible (softer) on touch — less interface machinery.
Tests: hero safety + lead reordering (70 total).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Show the typographic fallback for missing images too (not only on load
error), driven by component state instead of imperative class mutation —
which also clears the unused-CSS-selector build warning.
- Only render external links for http(s) URLs, else href=#.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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>