- Server-rendered /a/<id> "pointer" page (FastAPI): OG/Twitter meta from the
article's title/why/image, self-canonical (UB pages are the canonical for
themselves), a prominent "Read the full story at {source}" button and a quiet
"Explore more on Upbeat Bytes". No article body. Unknown/rejected/duplicate/
malformed ids → a calm 404 (no stack traces). Text-card preview when no image.
- Caddy routes /a/* to the API.
- Card Share control → menu: native Share… (where available), Copy link (the UB
card page), Copy source link. Boundary actions now hide-on-hover via a .mute
class so Save/Replace/Share stay visible. 122 tests pass.
(Event tracking for shares/opens lands with the analytics step next.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
History was logging every article merely displayed, which made it noise. Split
the two concepts cleanly:
- "displayed" (seenIds) still tracks everything shown, but only to stop Replace
recycling stories — it no longer feeds history.
- "history" now records only deliberate events: articles the user OPENED (card
click) or ones they REPLACED away (recoverable accidental swaps).
Also: per-item removal (× in the History panel; DELETE /api/history/{id}), and
when signed in the panel shows the account (cross-device) history. First-sign-in
import now folds the meaningful history (not everything shown). Copy updated.
115 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- API (auth-required): GET/POST/DELETE /api/saved (+/api/saved/ids), GET/POST
/api/history, POST /api/import — all FK-safe (skip ids that no longer exist).
queries.saved/saved_ids/history reuse the feed article shape.
- Frontend: reactive savedIds store (SvelteSet) + optimistic toggleSave; a Save
control on cards for signed-in users; a "Saved" view (You sheet) with its own
empty state; newly-seen items mirror to account history (cross-device); and a
one-time import folds this device's anonymous history into the account on first
sign-in. Anonymous browsing unchanged. 115 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Some sources hotlink-protect their images (e.g. Guardian's i.guim.co.uk → 401),
so a perfectly-enriched lead could still render an imageless hero. The browser is
the only true judge of loadability, so on a hero image error, promote the next
brief item that has an image into the hero slot; the failed lead becomes a text
tile. Resets to the lead on each fresh brief.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The accent line is now tinted by the article's primary topic (muted sand/sea/sun
tones), adding quiet variety across the grid. Falls back to the brand azure for
unknown/untagged topics.
- Raise the card-header height (84→94px) so the centered pills sit comfortably
clear of the accent line and divider.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Centering inside .tags could never look right: the accent line (.body::before),
its margin, and the body gap lived OUTSIDE the centering context, but the eye
measures the band from accent line to divider. Per Codex's audit, restructure
into one .cardhead unit — a fixed-height grid (accent row + a 1fr row that
centers the pill block) that owns the divider. Now the centered band is the band
you see, so 1-, 2-, and 3-pill cards sit evenly with aligned dividers and titles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
align-content:center is unreliable on wrap containers (a single wrapped line is
treated as single-line and ignored), which left pills top-aligned. Wrap the pills
in a .pillrow and vertically center that block with a column flex +
justify-content:center on the fixed-height zone — no single-line ambiguity. Pills
now sit evenly centered for 1-, 2-, and 3-pill cards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
54px barely cleared two rows, so 3-pill cards filled the zone edge-to-edge while
1-pill rows had slack. Raise the zone to 64px so the wrapped case keeps symmetric
top/bottom margins; centering then reads evenly across 1-, 2-, and 3-pill cards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
min-height was shorter than two rows of pills + padding, so two-row cards grew
taller than one-row cards and their dividers/titles dropped lower. Size the zone
to fully contain two rows and drop the asymmetric bottom padding; with centering,
single-row pills get even space above and below and every card's divider and
title line up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>