Single-row pill cards now sit centered in the two-row zone instead of pinned to
the top, so they balance visually against two-row cards. Titles already aligned;
this aligns the pills themselves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cards with a third tag that wrapped to a second row pushed their title/source
down, breaking alignment across the grid. Reserve a consistent two-row min-height
for the tag zone on tiles (pills top-aligned) and close it with a hairline, so
titles line up regardless of pill count. Hero opts out.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "wander" layer for the multi-tag model, sitting beneath the brief:
- Cards show up to 3 tappable grouping pills (the article's tags), falling back
to the primary topic for articles the re-tag hasn't reached. Tap a pill →
that tag's lane. Tags read as little doorways, not metadata confetti.
- New tag-lane view (select 'tag:<slug>' → /api/feed?tag=) with a calm heading
and the parent family's description as subtitle.
- Replace the flat "Explore by topic" strip with four calm family bands
(Discovery & Wonder / People & Kindness / Solutions & Progress / Mind & Craft)
from /api/families; zero-count tags hide until tagging fills them in.
- Mood nav stays the primary emotional layer; the brief stays the front door.
- /api/families fetch is non-fatal so the page degrades gracefully when the B1
backend isn't deployed yet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Header logo sized up to read clearly (54px desktop / 46px mobile, bars to match).
- Self-host Inter (variable, latin) — no external font calls — and use it for
the category tags as uppercase Light (300) kickers with tight tracking, for a
clean, polished label feel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add the real Upbeat Bytes lockup (logo.svg) and use it in the header,
replacing the placeholder inline mark + text wordmark.
- New square favicon: the logo's rising sun (bright gold) on azure.
- Recolor the design system around the logo's #0083ad azure: rename the
--sage* accent vars to --accent*, with deep/soft azure tints; navy ink
(#16263a) echoing the logo's "Bytes"; cool slate muted text; a deep gold
for text-weight accents plus --gold-bright for decorative fills; warm
sand paper background. No urgency colors.
- Retint the hero image overlay and the no-image card gradients to match.
- theme-color → azure.
Built clean; frontend tests/build pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Logo mark: SVG rising-dots wave (sage dots + warm gold peak = 'upbeat bytes'),
used as favicon/PWA icon and in the header.
- Header: full-width app bar — mark + mixed-type wordmark (Upbeat serif ink /
Bytes sans sage) on the left, housed Boundaries/History utility cluster on the
right (desktop). No more floating text links.
- Mobile: fixed bottom tab bar (Today / Browse / You); utilities move into a
'You' sheet. One-handed, modern, calm.
- Browse: moods stay the primary front door; added a quiet 'Explore by topic'
section (existing topics) below the content — selecting a topic loads its feed.
- Layout trimmed (header now in-page, full width); footer keeps clearance for the
bottom bar.
Phase A of the consensus pass; Phase B (add technology + learning topics and
reclassify) is next. Live site untouched until publish.sh.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Masthead, page <title>, PWA manifest name, and footer now say 'Upbeat Bytes';
README headline updated. The internal Python package/CLI stay 'goodnews' (no
functional reason to rename, and it avoids churn).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dismissed.size is read in the template (the History 'Clear' control), so the Set
must be $state for Svelte 5 to track .add()/reassignment. Build is warning-clean
again. Frontend only — rebuild + refresh.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per the agreed model: the brief is server-authoritative and a client Replace is
a soft override that yields when genuinely new data arrives.
- build_daily_brief is now idempotent: if the composed selection is unchanged it
leaves the brief (and its created_at) alone, so the timer's 15-min rebuilds are
no-ops when no new data landed.
- /api/brief exposes generated_at (the brief's created_at = a content-change
stamp). The client pins its view against generated_at and keeps it across plain
refreshes, but drops it and shows the fresh server brief when generated_at
advances. Missed stories remain in the mood feeds.
Tests: idempotent rebuild (no-op vs content change) — 93 total.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Persisting only 'dismissed' kept swapped-away stories out but let the brief
recompose on refresh — so a chosen replacement (and the hero) could change
unexpectedly. Now the reader's actual brief view is persisted per day:
- loadToday keeps the saved view for the same brief_date (swaps and hero hold
steady); re-fetches fresh on a new day or when forced.
- A boundary change forces a fresh re-fetch (and re-pins); Replace pins the new
view; Clear-session drops the pin so it re-composes fresh.
Frontend only — rebuild + refresh (no server restart needed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mood feeds now honor the same dismissed list as the brief: /api/feed accepts an
exclude param (over-fetching to stay full), and the client passes the persisted
dismissed set. Swapping a story away now keeps it gone everywhere — brief and
browse — not just on the home view. Also simplified the feed filter path to the
shared _prefs_sql_kw helper.
Tests: feed exclude (91 total).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A reader who swaps a story away should keep that swap after a refresh; before,
the server re-served the original brief.
- localStorage now persists seen / dismissed / history (loadJSON/saveJSON).
- /api/brief accepts an exclude list; dismissed (replaced-away) ids are dropped
and the highlights refill around them, so swaps stick and stay full.
- Replace records the swap to dismissed+seen and persists; the seen-set
(persisted) keeps Replace from recycling across refreshes too.
- History panel survives refresh and gains 'Clear what I've seen (start fresh)'
so it never feels suffocating. Saved history/favorites still come with sign-in.
Tests: brief exclude + refill (90 total).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 'npm run dev' now binds the network (vite dev --host) so the HMR dev server is
reachable from another machine.
- README documents the two-terminal loop (serve --reload + npm run dev via the
/api proxy), so iterating no longer needs build + restart + hard-refresh.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Hero blur fix: brief enrichment now prefers a page's og:image even when a
feed thumbnail exists (feed thumbs are often tiny; the hero is shown large).
Verified: BBC hero upgrades to the 1024px share image, ScienceDaily to 1920px.
- Today is now 'Highlights from Today' — hero + 6 (brief size 7), which also
makes the secondary grid a balanced 3+3 instead of an orphaned 3+1.
- Replace now excludes every article seen this session (a client-side seen-set),
so it never cycles back to something already shown.
- New session History panel (this tab only, no account): lists everything seen,
including swapped-away stories, so they stay recoverable. Persistent
history/favorites are tabled for sign-in later.
Tests: og:image upgrade of an existing feed image (86 total).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Composition (Codex's priority — content mix was the louder problem):
- _select_diverse now guards the daily five's emotional tone: at most 1 health,
at most 2 science+health combined, at most 2 of any topic, distinct sources —
so at least three of the five are community/culture/animals/environment when
available. Caps relax (mix, then source) only to fill on thin days.
- Verified live: today's five went to environment x2, health, animals, science.
UI:
- Source moved to its own line below the tags, left-justified, for uniform
rhythm across hero and tiles (was sometimes trailing the tags, right-aligned).
- Watermark kept as-is (intentionally subtle; liked).
Tests updated for the emotional-mix contract (80 total).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Frontend (the premium baseline):
- The hero is now the ONLY image slot. Soft feed images get an atmospheric
gradient overlay; no over-reliance on inconsistent RSS image quality.
- Every secondary/lane card is a uniform typographic editorial tile: no
thumbnails, equal visual weight, a faint topic wordmark watermark, a slim
sage top accent, consistent source, reason text as the trust signal, visible
Replace with quiet tuning actions. Fixes the jarring mixed-media row rhythm
and removes muddy thumbnails entirely.
Backend (composition):
- _select_diverse now balances topics: no more than 2 of one topic while other
topics have candidates (relaxing source then topic caps only to fill), so the
daily five stop clustering medical/science items. Candidates now carry s.topic.
Tests updated for the topic-balance contract (79 total).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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>
- Today is just the day's five highlights (hero + four) — the preview lanes are
gone; other categories appear only when their mood is selected.
- Each view leads with a large serif heading (Today / Wonder / ...) and a
subtitle, with a quiet sage rule — switching moods retitles the page.
- Drop the now-unused Lane usage from the home.
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>
- New BoundariesPanel.svelte: gentle, device-local controls. Avoid words/phrases
first (the trust-critical piece), then 'Paused for now' and 'Always hidden',
each with easy remove. Reassures 'nothing leaves this device'; adding a term
refreshes the brief/feed immediately.
- Quiet 'Boundaries' toggle (active indicator) replaces the old calm bar, keeping
the first viewport calm.
- Wording stays gentle throughout: avoid / pause / hide / boundaries — never
blocked/banned/blacklist.
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>