Commit Graph

167 Commits

Author SHA1 Message Date
thejayman77 489c34d2f2 Nav Back: track popstate delta so back-then-forward keeps accurate depth
Codex's remaining caveat: appNavDepth decremented on every popstate, so a
browser Back then Forward undercounted (in-page Back would jump to Highlights
early). Use the navigation's signed delta on popstate (Back -1, Forward +1,
±N for jumps) instead of a flat decrement, so the depth stays accurate through
any back/forward dance. Falls back to -1 if delta is unavailable (safe).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:15:43 -04:00
thejayman77 eb91a2f856 Nav hardening: app-safe deep-link Back + stale-load guard on Today
Per Codex audit follow-ups:
* Track in-app navigation depth (forward goto/link increments, popstate
  unwinds, clamped at 0) and base the in-page Back on it instead of
  history.length. A direct deep link (email/social/article) now sends the
  in-page Back to Highlights rather than out of the site.
* Apply the same stale-load guard to the Today/Highlights path that feed views
  have, and only scroll-to-top when the load is still current — avoids stale
  error/scroll state during quick navigation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:11:24 -04:00
thejayman77 dc245ab6ea Unify navigation: URL-backed views, one shared history
Per audit (user + Codex): the in-page Back and browser Back were two separate
histories, which is confusing — especially for less-technical users. Make the
URL the single source of truth so both traverse one history.

* The view derives from the URL (/?view=latest, /?tag=, /?source=, bare / for
  Highlights); `selected` is $derived from $page.url.
* All navigation goes through goto(); afterNavigate is the single loader hook,
  so in-app clicks AND browser back/forward reload the same way.
* The in-page Back button now just calls history.back() (fallback to Highlights)
  — identical to the browser Back. Removed the private navStack.
* Stop stripping ?source= — the URL stays honest, so source/tag views are
  shareable and survive reload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:05:57 -04:00
thejayman77 a8175db63e Back button: move inline-right; show on deep-linked source views; clean ?source
* Position the in-feed Back button to the right of the view title (inline with
  it) instead of stacked above; the accent underline moves under the title text.
* Deep-linking a source feed from an article page (/?source=<id>) now seeds the
  back history so the Back button appears (returns to Highlights).
* Strip the ?source= param after consuming it (replaceState) so it can't linger
  and make the browser back/forward behave oddly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 08:47:30 -04:00
thejayman77 8c52582ae3 In-feed Back button + clickable source on the article page
* Back button on feed views: drilling into a tag or source from a card now
  remembers where you came from (a small history stack), and a "← Back" appears
  in the view header to return there — chains of drill-ins included. Top-level
  nav (rail/bottom bar) resets the history.
* Article page: the source name is now a link into that source's in-app feed
  (/?source=<id>); the SPA reads the param on load and opens the source view
  (label falls back to the loaded feed's source name). Completes the
  "cards-only v1" — source is clickable on /a/ too now.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 08:38:44 -04:00
thejayman77 38889f76e5 Source feeds: click a source to see its publication feed
Click a source name on any card → a feed of just that source's articles,
newest-first, still accepted / non-duplicate / boundary-filtered (the calm
promise isn't bypassed). A natural way to follow a publication's feel.

* queries.feed + /api/feed: source_id filter; Article output gains source_id.
* Frontend: source label is a button → transient 'source:<id>' view (like
  'tag:<slug>'), rendered in the feed grid with Load more, header = source name.
* Ad-hoc, not a pinned lane. Foundation for a future source page (metadata) +
  Follow; shareable /source/<slug> route and source_view analytics come then.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 08:30:33 -04:00
thejayman77 8b44e559e4 Images Phase 3+4: roll tile treatment to Latest, topic/tag feeds, Saved
Carry the brief's uniform card language (compact photo banner or flat topic-
colored placeholder) onto every feed grid — Latest, topic/tag/mood views, and
the account Saved grid — so the whole site is visually consistent. Same quality
gate and uniform sizing; the brief hero remains the single large image.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:25:34 -04:00
thejayman77 224a95d64a Placeholder: settle on the clean flat word
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:23:20 -04:00
thejayman77 b9168b56ea Placeholder: revert drop-cap; add soft orb+ring graphic motif
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:21:17 -04:00
thejayman77 6435840966 Placeholder initial: bold uppercase drop-cap, larger
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:16:29 -04:00
thejayman77 86a6bd3b45 Card + article-page polish
* Placeholder: bold, slightly larger initial letter on the topic word; make
  health (teal) and environment (leaf green) clearly distinct and show more hue
  in the deepened word so they're easy to tell apart.
* Article page: the source name was chopped to its first word ("Read the full
  story at The") — use the full publisher name; open the source link in a new
  tab so upbeatbytes.com stays put.
* Use the new SVG back arrow on the account and admin top bars (matching the
  article page) instead of the old "←" glyph.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:05:32 -04:00
thejayman77 101ebb33c2 Remove now-dead .why CSS selectors
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:00:52 -04:00
thejayman77 e41fcf6cea Brief cards: drop "why" for more summary; flat placeholder, darker word
Per review: the summary and the "Why it's here" line were competing in a tight
card and both came up short. Remove "why" from cards (it still appears on the
article page when clicked in) and give the summary the room (image cards 2→4
lines). Also flatten the placeholder banner — solid topic-color tint instead of
a gradient-to-blank, with the topic word in a deep near-black shade of that
color so it reads clearly instead of washed out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:59:53 -04:00
thejayman77 d19099ee7d Images Phase 2: uniform brief tiles — photo or topic-colored placeholder
Per review, mixed photo/no-photo rows read as ragged ("the ones without feel
lacking"). Make every brief rest card carry a banner so the grid is uniform: a
compact 16:9 photo when available, otherwise a calm placeholder tinted by the
card's topic accent color (the same per-topic hue as the accent line) with the
topic word set faint in serif. A failed/blocked image falls back to the same
placeholder, so cards never look broken and heights stay identical. Hero remains
the single large image. Light desaturation on photos; no heavy tint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:13:18 -04:00
thejayman77 c25e14ed6a Add a permanent "Latest" lane beside "Highlights"
Restructure the nav around two permanent lanes, then the reader's chosen ones:
"Highlights" (the curated daily brief — formerly "Today") and "Latest" (the
freshest accepted stories, newest-first). Now that the gate is tight, a
chronological "incoming" feed is safe to expose.

* feed(): new sort="latest" (pure recency) alongside the default best-first
  rank; /api/feed exposes sort=ranked|latest (validated). Still accepted-only
  and boundary-respecting either way.
* lanes.py: two pinned lanes (Highlights + Latest) instead of one.
* Home: "Latest" view + "Load more" pagination for every feed view (offset-
  paged, de-duped). Mobile bottom bar gains a Latest tab.
* LanePicker shows both pinned lanes; nav rail renders them first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:56:48 -04:00
thejayman77 d87347b032 Dashboard: content + source-health; per-viewer local dates
* Date fix: introduce GOODNEWS_TZ (goodnews/localtime.py) so the brief's "today"
  rolls over in a pinned zone (Eastern) instead of UTC — robust to host-clock
  resets. The home page now formats the brief's date in each VISITOR's local
  timezone (from its UTC freshness stamp), so nobody ever sees "tomorrow."

* Admin "Content served": articles live, fresh (7d), ingested (24h), summaries,
  active sources, today's brief size — queries.content_stats().

* Admin "Source health": per active source, the failure streak, last error,
  accepted contribution, and computed next-poll time (so backoff / "resting
  until" is visible), via queries.source_health() reusing the feeds backoff
  math. Failing sources sort to the top; times render in the viewer's zone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:34:22 +00:00
thejayman77 452e5a3fe4 Hardening pass: scheduler backoff, FK cascade, a11y, test safety net
Pre-traffic cleanup from an audit:

* Scheduler: poll_due_sources now keys on the last *attempt* (success or
  failure), not the last success, and scales the wait by the consecutive-
  failure streak (capped at a day). A failing feed (e.g. Phys.org's HTTP 429s)
  used to be retried every cycle because it had no successful run; it now backs
  off and recovers on its own. Extracted due_source_rows() + tests.

* FK hygiene: deleting a daily_brief is supposed to cascade to its items, but
  SQLite enforces foreign keys per-connection — connect() already sets the
  pragma, so the cascade is correct going forward; added a regression test.
  (Orphaned items + Phys.org settings were cleaned directly on the live DB.)

* a11y: modal/drawer dialogs are now focusable (tabindex), close on Escape
  (window) and on backdrop click via a target check (dropping the inner
  stopPropagation handlers). Build is warning-free.

* tests: conftest points any un-mocked LLM client at a closed port with a 1s
  timeout, so an accidental real call fails fast instead of hanging the suite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:18:18 +00:00
thejayman77 978edc8f4a Mirror lane picker into the Account page
Add a "Lanes" section under Account that reuses LanePicker inline, completing
the round trip with Boundaries. Refactor LanePicker to support an `inline`
variant (bare panel vs modal) and apply changes immediately on toggle — so the
account panel needs no explicit save and the home modal now previews the nav
rail live as you pick. Selection still persists through the shared prefs store.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:26:38 +00:00
thejayman77 722bcf6317 Customizable nav lanes: pin moods / topics / discovery tags
Readers can now choose which quick-access lanes sit above the feed; "Today"
stays pinned. The pool (goodnews/lanes.py, served at /api/lanes) is one source
of truth over three lane kinds the feed already renders: moods, primary topics,
and high-volume Discovery tags. Selection lives in the existing prefs blob
(localStorage + /api/prefs sync); the filter parser ignores the new `lanes`
field, so it rides along harmlessly. Default = today's moods, unchanged.

Food/Space stay grouping tags rather than primary topics (per review): `space`
already existed; added `food` to the Mind & Craft family so the classifier
assigns it, and seeded the Food lane by re-tagging the two food sources.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:19:58 +00:00
thejayman77 ea58039fb9 SEO flywheel: /today digest, sitemap, robots, home OG tags
Make the summary pages discoverable so traffic compounds passively:
- /today: a server-rendered, shareable + indexable digest of today's brief —
  each item's title (→ /a summary), our summary, and a source link. OG/Twitter
  meta + self-canonical.
- /sitemap.xml: dynamic — home, /today, and every accepted non-duplicate /a page
  with lastmod. robots.txt allows all and points to it.
- Home (SPA shell) gains canonical + OG/Twitter tags for cleaner unfurls.
- Caddy routes /today + /sitemap.xml to the API. 133 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:37:05 +00:00
thejayman77 427210ac3e User feedback + expanded privacy-respecting admin stats
Feedback:
- feedback table; POST /api/feedback (anonymous-ok, optional category/email,
  honeypot + per-day flood cap) stores + emails the admin; GET /api/admin/feedback.
- Shared feedback store + FeedbackModal; a speech-bubble opens it from the desktop
  header, the mobile top bar (logo moves left), the footer, and /account. Feedback
  section in /admin.

Stats (additive, same privacy model — no IP/UA/referrer/raw terms):
- Event vocab: summary_viewed (fired on /a load), full_story (card → source),
  not_today/less_like_this/hide_topic, replace_used/replace_none, paywall_replace,
  paywalled_source_open. Card title/image opens /a (no double-count); history
  records via keepalive so it survives the nav.
- Dashboard: Accounts card (counts only), reading funnel (summary→source rate),
  emotional-mix & friction, paywall, returning-visitor buckets. (Health metrics
  deferred to a future monitoring dashboard.) 131 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:58:49 +00:00
thejayman77 cfde4e22db Summary briefing layer: Today pre-summarized, /a is the canonical read
Make summaries the core reading experience (summary-first, source-forward):
- Cycle pre-warms summaries for Today's 7 (idempotent → only new ones hit the LLM).
- /api/brief items carry their cached summary; Today cards (hero + tiles) show it
  inline, so Today reads as a calm briefing.
- Card title/image now open the /a summary page (the canonical artifact), with a
  visible "Full story" link straight to the source on every card (the escape hatch).
- /a gains related-grouping chips + a Copy-link/share control.
- Tighten the summary prompt: original, factual, no quotations / no close paraphrase.
Long tail stays lazy+cached. No article bodies stored. 129 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:48:32 +00:00
thejayman77 3924d927aa Consolidate Boundaries + History under the account; sectioned /account
The inline Boundaries/History panels lived on the home page, so opening them while
scrolled left you stranded. Move everything "yours" behind the account icon:

- Home header slims to: Saved (opens a right-side flyout, signed-in) · shield
  (Boundaries indicator — filled when active — linking to the Boundaries section) ·
  avatar. The inline panels + the home "saved" view are gone.
- /account is now a sectioned hub (left sidebar on desktop, top tabs on mobile),
  OPEN TO EVERYONE with each section self-gating: Profile (sign-in), Saved (sign-in),
  History (device/account), Boundaries (device/account), Admin (admins). This keeps
  Boundaries/History usable without an account (they're device-local) while
  consolidating the UI — and every section loads at the top, fixing the scroll bug.
- Lift Calm Filters and History into shared stores (prefs.svelte.js, history.svelte.js)
  so the home feed (applies/records) and the account page (edits/manages) share one
  source of truth. New SavedFlyout component. Card boundary actions only render when a
  handler is provided.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 01:59:53 +00:00
thejayman77 d1a4b24627 Fix: "Clear my history" now clears account history too
clearSession only reset the device-local history; for a signed-in user the panel
shows the account history (serverHistory), which was never cleared and never sent
to the server — so it looked like nothing happened. Add DELETE /api/history
(clear all) and have clearSession reset serverHistory + call it when signed in.
2026-06-04 01:09:43 +00:00
thejayman77 762f121320 Admin step B: stats endpoint + /admin dashboard
- users.is_admin (+ migration); admin = is_admin OR email in GOODNEWS_ADMIN_EMAILS
  (normalized). is_admin exposed on /api/auth/me. Server-authorized GET
  /api/admin/stats (403 for non-admins).
- queries.admin_stats: visitors (today/7d/30d), returning vs one-and-done, top
  opened articles, popular groupings + topics (derived from article_id at query
  time), share breakdown, daily opens/visits trend — all aggregate, no PII.
- /admin page (gated, redirects non-admins): stat cards, CSS bar lists, a daily
  trend; "Admin dashboard" link on /account for admins. 129 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:25:46 +00:00
thejayman77 1a778e1334 Admin step A: privacy-respecting first-party event logging
- events table (kind, article_id, visitor_hash, day) with a UNIQUE key that dedups
  to one row per visitor-day — caps volume and makes counts mean distinct
  visitor-days. NO ip/ua/referrer/url. Groupings derived from article_id at query
  time, never stored.
- POST /api/events (public): whitelisted kinds (visit/open/share_ub/copy_source/
  native_share/source_click); visitor token hashed server-side (never raw).
- Frontend analytics.js: random localStorage visitor token; track() via sendBeacon;
  visit once/day; open on article click; share_ub/copy_source/native_share from the
  share menu; /a landing pages fire source_click. 127 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:21:49 +00:00
thejayman77 1d71575982 Share pages: lazy, cached, our-own-words article summaries
The /a/<id> page now carries an original short summary so it stands on its own,
without republishing the publisher's article:
- summarize.py: transient SSRF-guarded fetch of the article text → local LLM
  writes a 2-4 sentence ORIGINAL summary (our words). Cached in article_summaries
  forever; we store only our summary, never the body. Generated lazily (only for
  shared/viewed articles), de-duped so concurrent hits don't double-generate.
- /a serves cached-or-pending; when pending it shows a calm "summary on its way,
  read at {source}" note and self-polls /api/summary/<id>, swapping the summary
  in the moment it's ready (never blocks the page on the batch-tier LLM).
- Share menu warms generation on open so recipients usually get the rich version.
- Container reaches the arbiter at arbiter:8080 over caddy_web (LLM env added to
  the API container). 124 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:08:40 +00:00
thejayman77 3d9900cdfc Article sharing: branded /a/<id> page + share menu
- 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>
2026-06-03 15:27:30 +00:00
thejayman77 15728c3bcb User avatar (Google picture), avatar in mobile You tab, /account page
- Capture the Google profile picture (picture claim) into users.avatar_url; an
  Avatar component shows it, falling back to the initial. Used in the desktop
  header and the mobile "You" tab (which now shows the user when signed in).
- Move account/settings to its own route /account (robust + scrolls to top),
  reached by the desktop avatar and the mobile You tab; drop the inline "You"
  sheet. AccountPanel gains a Sign out action; the page links to Saved/History/
  Boundaries via home intent params (?view= / ?open=).
- db: users.avatar_url (schema + idempotent migration). 118 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:41:43 +00:00
thejayman77 bb008cfaa5 Accounts Phase 4: prefs sync + account/settings panel
- Prefs sync: GET/PUT /api/prefs store Calm Filters/Boundaries on the account.
  On sign-in the client adopts the account's prefs if present, else seeds them
  from the device; every change PUTs to the account so tuning follows you across
  devices. (Login side-effects run under untrack so browsing doesn't re-trigger.)
- Account panel: GET /api/account (email, connected sign-in methods, saved count,
  active sessions); Export my data (GET /api/account/export → JSON download);
  Sign out everywhere (revoke all sessions); Delete account (cascades to all
  account data) with an inline confirm. Reachable from You → Account.

Deferred to a follow-up: link/unlink a provider (OAuth link-mode) and per-session
revoke. 118 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:02:38 +00:00
thejayman77 1aa250ca67 Rework history: opened + replaced only, with per-item removal
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>
2026-06-03 13:27:39 +00:00
thejayman77 409bb11444 Accounts Phase 3: save articles, account history, device import
- 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>
2026-06-03 12:56:31 +00:00
thejayman77 b635d8f574 Accounts Phase 2: Google sign-in (OAuth 2.0 / OIDC)
- oauth_google.py (stdlib): PKCE, auth URL, code exchange, ID-token claim
  validation (iss/aud/exp/email_verified — token comes straight from Google's
  token endpoint over TLS, so no signature re-verify / JWKS needed).
- API: GET /api/auth/google/start (302 to Google, PKCE + signed state cookie
  binding the flow to the browser) and /callback (CSRF-checked state, exchange,
  find-or-create by verified email → links to an existing magic-link account,
  session cookie, redirect home). Errors land on /auth/verify?error=google.
- SignIn modal: "Continue with Google" + an "or email link" divider.
- 112 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 01:31:52 +00:00
thejayman77 9237180608 Accounts Phase 1c: sign-in UI, magic-link landing, auth store
- Shared reactive auth store (auth.user) + postJSON helper (sends the cookie).
- SignIn modal: email -> "check your inbox" (calm, no password); Google slots in
  here in Phase 2.
- /auth/verify route exchanges the magic-link token for a session, then home.
- Header shows "Sign in" or an account avatar; the You sheet gains "Signed in as
  …" + Sign out (or a Sign in row). Anonymous browsing is unchanged.
2026-06-03 01:19:30 +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 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 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 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