Commit Graph

61 Commits

Author SHA1 Message Date
thejayman77 d2ae56dc65 Accounts Phase 1b: magic-link auth endpoints + sessions
- POST /api/auth/email/start — validate email, rate-limit, email a single-use
  magic link (identical reply regardless, so no account enumeration).
- POST /api/auth/email/verify — consume token, find-or-create user, open a
  session, set an httpOnly cookie (web) and return a bearer token (app).
- GET /api/auth/me, POST /api/auth/logout.
- Session resolved from cookie OR Authorization: Bearer; cookie is Secure in
  prod (https), relaxed for http so tests round-trip. CORS now allows POST.

Live SMTP send verified against the DNSExit relay (587/STARTTLS). 108 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 01:08:33 +00:00
thejayman77 6a514aa56b Accounts Phase 1 foundation: schema + WAL, auth core, email sender
Groundwork for self-hosted accounts (magic link + Google later), no third parties.

- db: account tables (users, identities, login_tokens, sessions, saved_articles,
  user_history, user_prefs); identities link multiple sign-in methods to one user
  by verified email. connect() now enables WAL + busy_timeout so the API can write
  account data alongside the host ingestion cycle.
- auth.py: users/identities (find-or-create + link), single-use magic-link tokens,
  opaque sessions — all secrets stored only as SHA-256 hashes.
- email_send.py: minimal STARTTLS SMTP sender + the magic-link email.

Secrets (SMTP, Google, session) live in the API container's env_file, not git.
API endpoints + sign-in UI come next. 105 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 01:02:24 +00:00
thejayman77 acbc06a9e5 Use BBC's clean image variant (cpsprodpb) instead of the branded one
BBC's og:image comes from the "branded_news" CDN path with a "BBC NEWS" logo
baked into the picture (shows as "…EWS" once the hero crops it). The identical
photo is served under "cpsprodpb" with no logo, so rewrite branded_news →
cpsprodpb. Best of both: full-resolution hero, no burned-in branding. Re-enriched
recent briefs so live images swap over. 99 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 07:51:51 +00:00
thejayman77 2145622b59 Stop rejecting BBC's branded_news images (the blurry-hero bug)
og:image extraction rejected any URL containing "branded_news" as a generic share
image, but that's BBC's normal CDN path for real article photos. So every BBC hero
fell back to the 240px RSS thumbnail (blurry when shown large). Drop that marker;
keep the genuine placeholder markers (facebook-default, og-default, etc.). Updated
the test to assert BBC branded_news paths pass through. 99 tests pass.

(One-time: cleared image_checked_at on the 57 previously-checked articles and
re-enriched recent briefs so existing thumbnails upgrade to og:images.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 07:47:08 +00:00
thejayman77 6d5bcb13e5 Fix stale pinned-brief images; enrich all 7 + retry failures
Root cause (Codex audit): the client pins the brief by generated_at, but image
enrichment populates image_url AFTER the brief is built without bumping
generated_at — so a verbatim pinned copy stays imageless even once the server
has the image. The reclassify rebuilt the brief and the early pin stuck.

- Frontend: when reusing a pinned brief (same generated_at), refresh server-owned
  metadata by article id (esp. image_url) while preserving the user's order and
  replacements. Re-saves the merged view so it stays current.
- enrich_brief_images: default limit 5 -> 7 (any brief item can become the hero
  via the client fallback or a replace, so cover the whole brief).
- Don't cache image failures forever: retry brief items still missing an image
  after a TTL (retry_days=2) instead of stamping them imageless permanently.

Pairs with the hero image fallback (dd0087b). 99 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 07:38:54 +00:00
thejayman77 dd0087b8b3 Hero falls back to the next image when the lead's won't load
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>
2026-06-02 07:23:11 +00:00
thejayman77 4211223e1c Color-code the card accent by topic + more breathing room
- 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>
2026-06-01 21:25:31 +00:00
thejayman77 f064b3f3fb Make accent + pills + divider one card-header unit (fixes pill centering)
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>
2026-06-01 21:21:31 +00:00
thejayman77 fd4229df83 Reliably center pills: wrap in a row, column-flex justify-center
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>
2026-06-01 21:10:52 +00:00
thejayman77 1e591f90c5 Give the pill zone room for even, centered margins
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>
2026-06-01 21:05:50 +00:00
thejayman77 37c23b634a Make the tag zone tall enough to equalize card heights
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>
2026-06-01 21:01:02 +00:00
thejayman77 940ba21476 Center pills within the reserved tag zone
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>
2026-06-01 20:58:02 +00:00
thejayman77 62f1a519a8 Add publish-web.sh for fast frontend-only deploys
Builds + rsyncs the static site without the API container rebuild/Caddy reload
that publish.sh does — for quick UI/CSS/copy iteration. Use publish.sh when
backend changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 20:55:12 +00:00
thejayman77 a7576e180a Align tile content: reserve a two-row pill zone with a hairline
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>
2026-06-01 20:54:11 +00:00
thejayman77 3f2c73b210 Phase B2: grouping pills + Explore-by-family (frontend)
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>
2026-06-01 20:46:22 +00:00
thejayman77 773b2f79fe Polish header sizing + refined Inter kicker tags
- 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>
2026-06-01 20:30:20 +00:00
thejayman77 0887b4b888 Rebrand to the azure logo + warm sand/sea/sun palette
- 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>
2026-06-01 20:13:02 +00:00
thejayman77 a47a1504c8 Phase B1: multi-tag groupings model (backend)
Three-layer organization: primary topic (one per article, for ranking and
brief balance) + grouping tags (1-4 per article from a controlled vocabulary,
the organic "wandering" axis) + tonal flavor.

- taxonomy: add technology + learning topics; 4 calm tag families
  (Discovery & Wonder, People & Kindness, Solutions & Progress, Mind & Craft)
  defined in code, not the DB; ALLOWED_TAGS union + coerce_tags validation.
- db: article_tags(article_id, tag) join table + tag index.
- llm: tags added to the classifier json_schema (enum-constrained, maxItems 4)
  and system prompt; normalize_scores coerces tags; upsert_article_score
  replaces a row's tags atomically on every (re)classification.
- queries: feed gains a tag filter and exposes tags via group_concat; tag_counts.
- api: Article.tags, feed tag param, and /api/families with per-tag counts.
- tests: coerce/normalize/upsert/tag-filter/reclassify-replace/tag_counts +
  /api/families. 99 passing.

Corpus reclassify (re-tag + new primary topics) runs separately against the
local LLM. Frontend (B2) pairs with this; the live site is unchanged until then.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 18:35:25 +00:00
thejayman77 c7f4db3973 Dev proxy targets the live API by default (no local backend needed)
npm run dev now proxies /api to https://upbeatbytes.com so the dev frontend
works standalone. Override with GOODNEWS_DEV_API=http://127.0.0.1:8000 to test a
local backend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 17:53:25 +00:00
thejayman77 c6d37039a8 Visual/IA pass: brand mark, real header, mobile bottom tabs, topic browse
- 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>
2026-06-01 17:28:25 +00:00
thejayman77 86975d599b Add deploy/publish.sh redeploy helper + document production split
One command to rebuild the frontend, sync it to the live Caddy site, refresh the
API container, and reload Caddy. README documents the upbeatbytes.com topology.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 02:20:29 +00:00
thejayman77 92fafa8785 Make the API read-only (healthz no longer runs init_db)
Lets the API run as a read-only replica against a shared DB owned by the
ingestion CLI — needed for the production split (Caddy-proxied API container
reading the host-written database).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 02:19:14 +00:00
thejayman77 f57b63edef Rebrand user-facing product to Upbeat Bytes (upbeatbytes.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>
2026-06-01 01:24:44 +00:00
thejayman77 9d257c9950 Make dismissed reactive ($state) to clear the Svelte build warning
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>
2026-05-31 17:08:58 +00:00
thejayman77 68a401eed6 Fresh server data overrides a pinned brief; pin holds otherwise
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>
2026-05-31 14:00:08 +00:00
thejayman77 f599f9d28e Pin the curated brief across refresh (stable, not dynamic)
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>
2026-05-31 13:51:00 +00:00
thejayman77 3fe7c4f228 Extend dismissed-exclusion to mood feeds for consistency
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>
2026-05-31 13:29:27 +00:00
thejayman77 0ccd5554d2 Persist replacements across refresh (device-local, no account)
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>
2026-05-31 13:22:41 +00:00
thejayman77 803da64e16 Personalized brief: refill to full count when a boundary hides a highlight
When a reader's boundary (avoid-term, muted topic/flavor, pause) removes a brief
item, top the highlights back up with other readable, boundary-respecting good
news instead of showing fewer cards — so 'Highlights from Today' stays full and
still honors what they don't want to see. (Reverses the earlier filter-down-only
MVP, now that the count is fixed at seven.)

- /api/brief: after filtering by prefs, refill from the accepted pool (same
  categorical SQL filters + avoid-terms) excluding already-shown items.
- Shared _prefs_sql_kw helper for feed/replacement/brief filters.
- Tests: refill stays full and respects mute + avoid-terms (89 total).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 13:13:54 +00:00
thejayman77 e26831473c Dev workflow: network-bound vite dev + documented hot-reload loop
- '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>
2026-05-31 13:07:23 +00:00
thejayman77 7e1dfd5b3c Reject branded/generic share images; hero prefers a clean illustrated story
- og:image enrichment now skips branded/generic share images (BBC 'branded_news'
  with its burned-in logo, NPR 'facebook-default', etc.) and keeps the first
  real article image — so no competitor logo lands on our hero. Cleared the few
  already-stored branded URLs so they re-enrich.
- Hero selection now prefers a gentle + readable story that also HAS a (clean)
  image, falling back to gentle-readable, then gentle. The lead is visual when
  possible, typographic otherwise — never branded.

(The '7 cards' report was a stale browser cache: the brief stores 7 and the
built JS requests 7; a hard refresh shows all seven.)

Tests: branded/generic image rejection (87 total).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 13:03:20 +00:00
thejayman77 d8d665ee35 Crisp hero (prefer og:image), 7-card Highlights, no-recycle Replace + session History
- 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>
2026-05-31 12:56:57 +00:00
thejayman77 9e8eddf46d Bounded hero-image enrichment (og:image for brief items only)
The grid stays typographic; the hero is the one intentional visual slot. At
brief-build time we fetch a hero-quality image for the daily five that lack one:
- enrich.py reads ONLY a page's <head> og:image/twitter:image and stores just
  the URL (never the body).
- SSRF-guarded: http(s) only, 6s timeout, 300KB cap, <=3 manual redirects each
  re-validated, and hosts rejected if any resolved address is private, loopback,
  link-local, multicast, reserved, or unspecified.
- image_checked_at column caches success AND failure, so an article is never
  retried forever.
- Wired into build-brief and cycle (brief items only, only if image missing and
  unchecked). Everything else stays metadata-only.
- Verified live: today's five all carry images (feed + enriched).

Tests: og:image parser, head-only scope, IP guard across internal ranges, and
enrich success + failure-caching (85 total).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 12:37:41 +00:00
thejayman77 3858380ffe Brief emotional-mix guardrails + source on its own line
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>
2026-05-31 12:29:02 +00:00
thejayman77 541f59ed6e Option A: typographic editorial tiles + single treated hero image; balance brief topics
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>
2026-05-31 12:10:05 +00:00
thejayman77 ba801d90f6 Make paywalls systemic + fix ArticleCard reactivity
- 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>
2026-05-31 01:36:53 +00:00
thejayman77 bfd612eb9b Paywall awareness (#6) + replace-an-article (#7)
- 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>
2026-05-31 00:39:13 +00:00
thejayman77 06c2704ae0 Home: Today shows only the five; categories behind mood selection; big view heading
- 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>
2026-05-31 00:22:34 +00:00
thejayman77 e8c4bb6e8b Even out card heights: description only on hero, clamp lane card text
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>
2026-05-31 00:04:50 +00:00
thejayman77 f46fee1197 Typographic-first imagery + opportunistic feed-HTML image extraction
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>
2026-05-30 23:59:36 +00:00
thejayman77 b9ecebffde Add Boundaries panel to the UI: precise personal avoid-terms first
- 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>
2026-05-30 22:50:11 +00:00
thejayman77 15d51fb8fd Hero emotional-safety guardrail + calmer card polish
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>
2026-05-30 22:44:00 +00:00
thejayman77 14842127da Fix ArticleCard image fallback, build warning, and link safety
- 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>
2026-05-30 22:39:22 +00:00
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
thejayman77 1e190c5e88 Advisory source health: review flags, never auto-deactivate
- Add source health columns (last_success_at, last_error_at, last_error,
  consecutive_failures, review_flag, review_reason) via SCHEMA + migration.
- poll_source maintains them: success resets the failure streak and records the
  success time; failure increments it and stores the latest error.
- review_sources() flags active sources that are stale, repeatedly failing,
  low-acceptance, duplicate-heavy, or doom-skewed (high cortisol/ragebait) over
  a recent window. It is purely advisory: it sets review_flag/review_reason and
  never changes the active column (human stays in the loop), clearing the flag
  when a source recovers.
- CLI review-sources; cycle runs it as a final step (--no-review to skip);
  source-report shows a review line for flagged feeds.
- Tests: healthy/failing/stale/low-acceptance/recovery and never-deactivates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:28:35 +00:00
thejayman77 aa4125ddec Supervised source candidates: stage, list, promote, reject
- New source_candidates staging table (status suggested/quarantined/rejected/
  promoted, preview_json snapshot) so untrusted/suggested feeds stay out of the
  real ingestion path until reviewed.
- sources.py: save_candidate (re-preview never revives a curator's rejection),
  list_candidates, reject_candidate, promote_candidate (copies into sources,
  inactive by default — active on approval; never automatic).
- CLI: suggest-source / list-candidates / promote-candidate / reject-candidate.
- API: read-only GET /api/candidates (writes stay CLI-only — no unauthenticated
  public write surface yet).
- Fix deprecated ElementTree truth-value test in _parse_rss.
- Tests: candidate lifecycle (save/list/promote/reject, status preservation,
  name derivation) — 51 total.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:52:40 +00:00
thejayman77 95195daff8 Track 3: read-only source preview (vet a feed before adding)
- feeds.preview_feed(): fetch + score a sample WITHOUT persisting; returns
  freshness, acceptance rate, cortisol/ragebait/PR averages, and example
  accepted/rejected items. With an LLM client it also returns topic/flavor mix
  and the model's (accurate) acceptance view.
- CLI 'preview-source URL [--sample] [--classify]'.
- API 'GET /api/source-preview?url=&sample=&classify=' with an http(s)-only
  guard (SSRF note left for go-public hardening).
- Site 'Suggest a source' panel with Quick check (heuristic, instant) and Deep
  check (model, accurate), rendered DOM-safely.
- Tests: network-free preview_feed tests via monkeypatched fetch (45 total).
- README documents the command, endpoint, and updated roadmap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:37:34 +00:00
thejayman77 cabe0b6049 Harden Calm Filters surface before Track 3
- Add API test layer (TestClient): bad prefs -> 200, mute affects feed,
  avoid-term filters, brief filters down, counts match filtered feed.
- Render article cards via the DOM API (textContent) instead of HTML string
  interpolation, and only allow http(s) hrefs — defense-in-depth XSS guard for
  when the feed faces untrusted sources publicly.
- Refresh the stale README Next Steps to reflect what's done vs ahead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:31:45 +00:00
thejayman77 091dec64ae Calm Filters MVP: device-local personalization across feed/brief/counts
- API endpoints (feed, brief, category-counts) accept a 'prefs' JSON query
  param, parsed tolerantly into FilterPrefs (bad blobs never break the feed).
- Feed over-fetches then applies word-boundary filters in Python and slices to
  the page; brief is filtered down (no refill); counts are computed over the
  same filtered set so browse numbers match the feed exactly.
- Pause.active() coerces naive datetimes to UTC; FilterPrefs.from_dict skips
  malformed pauses and non-string list entries.
- Static site adds the humane ladder (Not today / Less like this / Always hide)
  plus a Calm filters panel managing pauses, mutes, and avoid-terms in
  localStorage. Nothing leaves the device.
- Tests now 38 (added forgiving-parse and naive-now cases). README documents it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:16:42 +00:00
thejayman77 9cdcda5e02 Durability pass: tests, clearer diversity/classify behavior, Calm Filters foundation
- Add pytest suite (34 tests) covering scoring thresholds, dedup clustering +
  representative selection + time window, brief source/category diversity,
  avoid-term phrase matching, and text canonicalization/truncation.
- Rewrite _select_diverse with an explicit, tested contract (best-first, one
  per source, backfill, then inject a second category by evicting the
  lowest-ranked pick).
- classify_articles now returns attempted/succeeded/skipped (ClassifyReport) so
  silent model failures are visible in both the cycle and classify output.
- Fix clean_text truncation to stay within max_len (ellipsis no longer
  overshoots).
- New filters.py: canonical FilterPrefs shape (include/mute topics+flavors,
  avoid_terms, pauses) and pure word/phrase-boundary matching engine seeding
  Calm Filters. Not yet wired into the API.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:07:31 +00:00