Commit Graph

354 Commits

Author SHA1 Message Date
thejayman77 c33dad9832 images: add Pillow to the web extra so the API container downscales too
The on-demand /api/img path runs in the container (only fastapi+uvicorn), so without
Pillow it fell back to caching the original full-size bytes instead of a downscaled
WebP. Add Pillow>=10 to the web extra. The host cycle already had it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:30:05 -04:00
thejayman77 8a3c00db3b images: cache + serve article images from our own origin (bounded, LRU-evicted)
Stop hotlinking news images from third-party CDNs (the source of the "blank until
you refresh a few times" graphic). New goodnews/newsimg.py caches a downscaled WebP
display copy (≤800px) beside the DB, like art_cache:
- GET/HEAD /api/img/{article_id} — resolves id→image_url (allowlisted to our corpus,
  not an open proxy), fetch+cache on first miss, serve local after, immutable headers.
- cycle warms display copies for recent accepted-with-image articles (so the FIRST
  view is already local) and prunes to a hard size cap (default 1 GB) by LRU eviction.
Frontend now points at /api/img/<id>: the hub lead, every ArticleCard (feed hero +
cards), and the /a/<id> share page's visible image. og:image/twitter:image stay the
source URL so social crawlers fetch the canonical image directly.

Storage is bounded by construction — over the cap, least-recently-used files are
evicted, so it can't grow without limit regardless of ingest rate. Tests cover
fetch/downscale, cache-hit (no refetch), bad-scheme/non-image rejection, fetch
failure, LRU prune, warm, and the endpoint allowlist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:28:33 -04:00
thejayman77 cb06d550bd home: reveal the news photo only once it actually loads (retry + graceful fallback)
The hub painted the lead news image as a CSS background straight from the source's
hotlinked URL — one transient failure (slow/rate-limited third-party CDN) left a
blank plate until you refreshed and the browser served it from cache. Now the probe
that already runs for cover-vs-figure detection gates the photo: load with up to two
retries (0.5s/1s backoff), reveal the plate only once it's truly loaded (and cached),
and otherwise keep the typographic topic cover. Soft fade-in on arrival; reduced-motion
honored. No more blank-until-refresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 19:58:26 -04:00
thejayman77 d98cec9ded admin: read/unread triage for load errors (unread by default, mark read/all)
The load-error log had no way to clear reviewed entries. Add a read_at column to
client_errors and a read/unread model mirroring the feedback inbox:
- GET /api/admin/client-errors?show=unread|read|all (default unread; returns id+read)
- POST /api/admin/client-errors/read-all  (mark all unread read)
- POST /api/admin/client-errors/{id}/read {read: bool}  (per-row toggle)
Headline stat is now "Unread load errors" (admin_stats.client_errors.unread), so the
red badge clears as you triage. Admin UI: Unread/Read/All tabs, a "Mark all read"
button, and a per-row ✓/↩ toggle; reading an entry drops it from the default view.
14-day auto-prune still bounds the table. Tests cover filter, toggle, mark-all,
404, and gating.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 10:38:22 -04:00
thejayman77 bddb8d22b0 HubBar: revalidate auth on mount so the avatar shows on cold hub entry
auth.user paints from its localStorage cache, but if the hub is the entry point
nothing had refreshed the session. Revalidate once (guarded on !auth.ready) so the
profile picture + signed-in state are correct wherever the shared bar renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 05:56:26 -04:00
thejayman77 b8ac82e897 HubBar: show the signed-in profile picture in the account button
The cutover put HubBar on / and /news, but HubBar's account icon was a hardcoded
person SVG — so signed-in users lost the avatar the old `/` Header showed. Render
<Avatar> when auth.user.avatar_url exists (fills the circle, no tint peek); fall
back to the person glyph when signed out / no picture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 05:54:11 -04:00
thejayman77 0ae789752e fix: QOTD/WOTD freshness — pick within the freshest cohort, not the rotated pool
Both selectors ordered candidates least-recently-shown, then daily.seeded_order()
ROTATED the whole list and took [0] — an arbitrary date-hashed item, undoing the
ordering. Result: repeats (quote id 2 on 6/28+6/29; word "harmony" on 6/25+6/28),
no guarantee a pool item is shown before it recurs.

Fix: daily.freshest(rows) returns the freshest cohort only — every NEVER-shown
item while any remain, else the oldest-shown group. quote/wotd _candidates use it;
seeded_order now picks deterministically WITHIN that cohort. So every pool item is
featured once before any repeat, then cycles oldest-first. Dropped the unused
_NO_REPEAT_POOL window. Tests: no-repeat-until-exhausted (quote + wotd) + a
freshest() unit test. 428 backend tests green.

(Separate follow-up: expand the QOTD pool from 16 → 90+ vetted public-domain
quotes for a longer no-repeat window.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 05:39:06 -04:00
thejayman77 414a4c4b8b deploy: drop the cache-warmer from sync-static.sh (no-op without CF proxy)
Cloudflare is DNS-only (grey-cloud) for upbeatbytes.com — no proxy/CDN/edge — so
the warm() step (curl every chunk + key routes through the public domain) wasn't
priming any edge; it just GET every asset from the already-fast static origin,
generating thousands of internal-origin requests per deploy (the "traffic spike"
in the logs). Removed it. Kept the valuable part: chunks-before-shell ordering,
14-day chunk grace, service-worker last. No change for visitors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 05:28:49 -04:00
thejayman77 03aed9c37d fix: mobile-game footer selector + hub teaser honors Boundaries (Codex)
- app.css: the playing-game footer-hide targeted the old footer.site; the shared
  footer is footer.ub-foot now → during a mobile game the footer lingered. Retarget.
- Homepage hub teaser fetched /api/brief without the reader's prefs, so an excluded
  topic could still be featured on /. initPrefs() + append P.param(prefs.data),
  matching the News Brief — boundaries now respected on the hub.

(Nonblocking, noted for later: legacy /?view=… redirects are client-side and drop
unrelated params like UTM.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 20:05:36 -04:00
thejayman77 667b1a82c3 brand: standardize "Upbeat Bytes" → "upbeatBytes" everywhere
Per the logo + brand: the name is upbeatBytes (camelCase). Swept all user-facing
strings — titles/og:site_name/og:title, logo alt text, share pages (share.py),
emails (email_send), classifier prompt (llm), digest/unsubscribe (api), PWA
manifest, game share text, sign-in, the SPA shell + patch-static-heads (play
title) — plus README/publish.sh and the email test fixture. (SMTP From env was
already upbeatBytes.) Domains (upbeatbytes.com) unchanged. 425 BE + 36 FE green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 20:01:20 -04:00
thejayman77 f8628b3b14 homepage: title → the hub tagline (was news-only)
<title> + og:title + twitter:title: 'Upbeat Bytes — calm, constructive news' →
'upbeatBytes — a calmer, brighter corner of the internet', matching the hub's own
svelte:head so crawlers and JS users see the same thing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:55:13 -04:00
thejayman77 1bd86e30e5 caddy: fix /home2,/home3 redirect (redir destination, not a path matcher)
`redir / permanent` mis-parsed — Caddy read the leading `/` as a path matcher and
`permanent` as the destination, so it only matched `/` and emitted a broken 302 to
"permanent". Use an explicit destination URL (matching the www→apex idiom):
`redir https://upbeatbytes.com/ permanent`. Live Caddy reloaded; snapshot mirrored.
Verified: /home2,/home3,/home3.html → 301 → /.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:21:44 -04:00
thejayman77 2cfffdfd6a NEWS RELAUNCH CUTOVER: promote the hub to /, feed to /news, go public
The big flip. /home3 (hub) becomes /; the feed lives at /news; both indexable.
- PROMOTE: routes/+page.svelte is now the hub (was the interim NewsFeed wrapper);
  noindex removed; "Read more good news" → /news. routes/home3 + home2 deleted.
- routes/+page.js: redirects legacy root-query links (/?view=latest, /?tag, /?source,
  /?q, /?view=today→highlights) to /news before the hub renders (no flash).
- /news: noindex dropped (route meta + Caddy @newsHidden removed); now public.
- LINKS: HubBar brand/Home → /, News default → /news; HubShell/art/play back → /;
  account Following + share.py Explore/Browse/source → /news.
- FOOTER: one shared Footer.svelte (motto + Send feedback + slot) across Hub/News/
  Play/Art/HubShell/Account/Zen; global layout footer removed (FeedbackModal stays).
- SITEMAP: + /news /art /play /word /quote /onthisday; cap 5k→50k; gated on
  has-summary; paywalled excluded; HEAD now 200 (api_route GET+HEAD).
- Head-patcher: /news entry. PWA + shell description broadened to the hub.
- Caddy: @newsHidden dropped; @hidden now admin-only (word/quote/onthisday public);
  /home2,/home3 → / 301. Mirrored to deploy/caddy snapshot.

425 backend + 36 frontend tests green; build clean; Caddy valid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:16:43 -04:00
thejayman77 1c1ecefde8 news: harden paywall exclusion at the candidate query + add the missing regressions
Codex's two non-blocking hardening items, folded in before cutover:
- _candidate_articles() now excludes paywalled sources IN-QUERY (before LIMIT 50),
  so flagged stories can't consume candidate slots and leave a full brief thin.
  Dropped the now-redundant post-fetch filter in build_daily_brief.
- Regressions: history retains a viewed paywalled article; sitemap omits a
  paywalled source AND restores it under override="free".
- Aligned test_brief_paywall to the source-level model (paywalled sources carry a
  paywalled homepage, as in production) — it had relied on article-URL detection.

425 backend tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 18:54:53 -04:00
thejayman77 c600145ba5 news: close the remaining no-paywall bypass paths (Codex audit)
queries.feed was the main chokepoint, but several discovery paths have their own
SQL. Apply the shared source exclusion to all of them so "no paywalls" is truly
site-wide:
- briefs.build_daily_brief: EXCLUDE paywalled candidates (was: demote) — never
  stored in a new brief.
- queries.brief: stored-brief retrieval (covers /today + /api/brief) filters the
  paywalled source.
- digest.digest_items + followed_digest_items: the morning email + "from what you
  follow" omit paywalled sources.
- sitemap(): paywalled article pages excluded from the sitemap.
All reuse queries.paywalled_source_ids (admin override still wins).

Regression tests (test_paywall_exclusion.py): never stored in a new brief; /today
+ digest omit it; followed-source email omits it; Saved retains it; 'free'
override restores eligibility. 423 backend tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 17:22:52 -04:00
thejayman77 0d21231597 news: hard-exclude paywalled sources from the feed + brief (no unreadable news)
Per Jay: don't surface stories people can't read without paying — it's off-brand
("no paywalls") and pointless. Paywalled is source-level (domain rule, admin-
overridable): just 3 sources today (Nature, New Scientist, MIT Tech Review),
~5.4% of accepted articles.

- queries.paywalled_source_ids(conn): live source set (admin override wins).
- queries.feed gains include_paywalled=False (default) → adds `a.source_id NOT IN
  (…)`. One chokepoint covers Latest/tags/sources/moods/topics/search/since AND
  the brief top-up. Source-level + SQL → paging stays exact, no frontend change.
- brief(): filter the cached/home pool by the same rule; replacement already
  avoids paywalled and now rides the feed exclusion too.
- Dropped the now-moot "paywalled below readable" demotion sort.
- Saved/history keep showing items you saved (their own queries, not excluded).
- test_source_paywall_override updated: paywalled source → excluded from the feed
  (was: shown with a badge); 'free' override → returns, no badge. 418 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 17:10:00 -04:00
thejayman77 54761f5083 news behavior split: /news leads with Latest, Highlights via ?view=highlights
Base-aware so the frozen `/` is untouched (it's still the live, indexed site
until cutover); the new behavior applies only to /news. At cutover `/` becomes
the hub and only /news's behavior remains.

- defaultView(base): /news bare → Latest (the live firehose); `/` bare → Highlights.
- Brief is canonically /news?view=highlights, with ?view=today kept as an alias.
- Latest is pure chronological on /news — stop passing `home` into it (geo scope
  belongs to Highlights). The Closer-to-Home card/dial is hidden on /news Latest;
  Highlights keeps the scope dial. `/`'s Latest keeps geo (frozen).
- Back fixed: on /news it shows only for genuine drill-ins (tag/source/search),
  not the top-level lanes (Latest/Highlights/Following); `/` keeps its old rule.
- goBack's app-safe fallback lands on the base's default view.

feednav.js gains defaultView + def-aware parse/build; 36 frontend tests (9 new),
build clean. /news stays noindex.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 16:21:42 -04:00
thejayman77 39b38f0cf1 /news utilities: label the pills + wrap the action row on phones (Codex)
- Saved/Boundaries are now labeled pills (icon + text), not bare circles — a
  shield alone doesn't read as "Boundaries" on touch. .vh-util is auto-width with
  padding; labels show on desktop and mobile.
- Fix the narrow-screen overflow: on a signed-in hub-chrome drill-in the row
  (Search+Saved+Boundaries+Follow+Back) exceeded ~320–375px. The view-head now
  wraps the action row below the heading — scoped to `.container.hub` so the
  frozen `/` feed (fewer controls; Saved/Boundaries in its Header) is untouched.

32 tests green; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 16:01:05 -04:00
thejayman77 036e7ed7e8 /news: surface Saved + Boundaries in the view-head (don't bury them in Account)
Per Codex: HubBar stays purely site-level, so the feed's own utilities live with
the feed. Beside the existing Search toggle (hub chrome only, so `/`'s Header
keeps its own — no duplication): a Saved button (opens the existing flyout) and a
Boundaries/Tune control with a visible active indicator (links to its account
section for now). Same pill styling as Search.

Also flagged the Back-condition trap in-code: once bare /news becomes Latest,
Back must be suppressed for 'latest' too (only genuine drill-ins show it) — to be
fixed at the behavior split, not now (would alter the frozen `/`).

32 tests green; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 15:42:40 -04:00
thejayman77 e974fc4942 /news: wear the shared HubBar (consistent chrome), keep BottomNav + global footer
Per the agreed direction (Codex calls): /news joins the new family without CD.
- NewsFeed gets an explicit chrome="legacy|hub" prop (never inferred from path):
  `/` passes legacy (its own Header, unchanged) and /news passes hub (the shared
  editorial HubBar). Exactly one bar renders — never HubBar + Header.
- HubBar gains a configurable `newsHref`; the /news instance links News → /news
  (active), not the live `/`. Other hub pages keep the default (News → /).
- BottomNav kept (Highlights/Latest/Play/You stay visible); no top-level Back on
  bare /news (HubBar Home returns to the hub); contextual Back on drill-in views
  is unchanged. No new footer — the global footer stays until the shared Footer step.

Known prominence shift (refinable later): Saved/Boundaries move off the top bar on
/news (reachable via account); Feedback stays via the global footer. /news still
noindex. 32 tests green; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 15:32:08 -04:00
thejayman77 2fd28fa719 news: track @newsHidden in Caddy snapshot + extract testable feed routing helpers
Housekeeping per Codex:
- Mirror the live @newsHidden rule into deploy/caddy/Caddyfile.snapshot so the
  /news noindex protection is reproducibly recorded.
- Extract the feed's routing helpers (feedBase/parseView/viewUrl) into pure
  $lib/feednav.js and unit-test them (the base-aware URL generation wasn't
  exercised by the prior suite). NewsFeed imports them; behavior unchanged.

(Note: the step-1 commit also swept in data/wotd_audio/renewal.mp3 — a legit
cached pronunciation, not extraction-related; left as-is per Codex.)

32 frontend tests green; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 15:19:36 -04:00
thejayman77 f4a7a7bcc7 news relaunch step 1: extract the feed into NewsFeed.svelte, mount at / and /news
Pure refactor, no visible/behavioral change. The ~1065-line feed moves verbatim
from routes/+page.svelte into lib/components/NewsFeed.svelte; both routes/+page
and the new routes/news render it. Link generation is base-aware (feedBase()):
on `/` it builds `/?…` exactly as today (bug-for-bug parity); on `/news` it
builds `/news?…` so /news is self-coherent. At cutover, `/` becomes the hub and
the feed lives only at /news.

/news is kept hidden during the transition (noindex, follow) so we never publish
a duplicate indexable feed: route <meta robots> + a Caddy @newsHidden X-Robots-Tag
(follow, so link equity flows). Removed at cutover, when /news enters the sitemap.

27/27 frontend tests green; build clean; /news.html prerenders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 14:11:06 -04:00
thejayman77 099bf55711 docs: news relaunch migration plan (link/redirect map + interim routing)
Settled plan (user + Codex) for standing up /news as the feed's home and cutting
/home3 → / without breaking the feed, deep links, or SEO. Drives the upcoming
implementation; next build is the feed extraction (pure refactor). Includes the
four Codex amendments: /news noindex during transition, explicit prototype 301s,
explicit legacy-view mapping (shim before render + /news?view=today alias), and
the footer coverage inventory (FeedbackModal stays in the global layout).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 14:00:08 -04:00
thejayman77 6c10ad99a9 On This Day: serve sharp images (originalimage, not the 330px thumbnail)
The Wikimedia feed's thumbnail is 330px, which upscales blurry in our hero. Use
originalimage.source instead — it's reliably sharp. (Can't just request a bigger
thumbnail width: for very large source images Wikimedia only serves pre-generated
bucket sizes and 400s on arbitrary widths — e.g. 500px ok, 800/1024px fail.)

- onthisday._best_image() prefers originalimage, falls back to the thumbnail.
- scripts/otd_image_upsize_backfill.py re-fetches each stored MM-DD and upgrades
  image_url in onthisday_pool + daily_onthisday in place (ran on host: pool + 6
  daily rows now sharp; today's hero verified 200). Only the /onthisday hero
  loads this image (home card is text-only), so larger files are a single-page,
  one-time load.
- test_best_image locks the prefer-original/fallback behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:07:37 -04:00
thejayman77 e3e6f24753 home3 news: typographic category cover for pictureless articles
~half the brief has no image, leaving a blank well above the headline. When an
article has no image_url, fill the well with the topic word (e.g. "science") in
lowercase Newsreader on a soft topic-tinted field, color-coded per topic
(science/tech/environment/health/community/culture/world/space + neutral
default). Same 5:4 footprint as the photo, so card height stays consistent.
Threads `topic` through the news object.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:05:41 -04:00
thejayman77 022908392b /onthisday: IN HISTORY +1px (final landing)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 21:38:13 -04:00
thejayman77 998e758614 /onthisday: drop "IN HISTORY" 2px to land it (final)
top: calc(0.2 * var(--ys)) → + 2px.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 21:36:16 -04:00
thejayman77 14c2648f8f /onthisday: tie "IN HISTORY" offset to the year size (stop the oscillation)
6px read high, 12px read low — instead of guessing another px, anchor the label
to the visible cap top via top: calc(0.2 * var(--ys)). ~9px at the 46px desktop
year, scaling down on mobile. Still absolute, so the baseline is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 21:30:17 -04:00
thejayman77 fe0c2988c2 /onthisday: lower "IN HISTORY" to the visible top of "2013" (cap-offset fix)
Per Codex: top clamp(4px,0.7vw,6px) → clamp(9px,1.5vw,12px). Label is absolutely
positioned, so this only moves it down ~6px to meet the numerals' visible cap —
the year/date baseline and the rest of the row are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 21:25:22 -04:00
thejayman77 883c37b428 Joy cards finalize (Codex pass): robust year align, image guard, a11y, honesty
/onthisday:
- Year alignment restructured per Codex: "2013" is the sole in-flow baseline
  anchor; "IN HISTORY" is absolutely positioned in reserved start-padding, so it
  can't drag the shared baseline (no more viewport-math/top-offset compensation).
  Rule baseline-aligns too. Narrow phones deliberately wrap the year lockup onto
  its own row (rule+date below) instead of accidental flex wrapping.
- Render the hero only when image_url exists (pool has imageless items → was a
  blank dark hero).
- "The story" → "A little context": the Wikipedia summary is context about an
  associated subject, not a narrative of the event. Honest without backend work.
- Figure detection also forces contain on filename hints (seal/flag/logo/map/
  diagram/crest/emblem/coat-of-arms) so JPEG logos/maps aren't cropped.

A11y contrast (AA): clay #b06a45→#9a5a38 (eyebrow, button, + homepage card
accent so they still match), small green IN HISTORY #3a7d5b→#367653, rose
attribution/share + Copy button → #8b596d.

/quote: native Share now carries the attributed text (author included, matching
Copy); narrow-phone guard so "QUOTE OF THE DAY" + eyelash don't crowd at 320px.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 21:10:11 -04:00
thejayman77 36b3df5d40 /onthisday: nudge year alignment + clay Read-more button
- Raise "2013" 2px toward the line; lower "IN HISTORY" ~3px (offset constant
  8→11) so its top sits flush with the numerals instead of riding high.
- Read-more button green → clay (#b06a45) to match the "On This Day" title, per
  Jay giving each card its own character.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:36:25 -04:00
thejayman77 719a2c5052 /onthisday: baseline-align "2013" with the date so it sits on the line
The chaos was align-items:flex-end + a tight 0.86 line-height letting "2013"
hang below the date's line. Switch the dateline to align-items:baseline so the
year and "Friday, June 26" share one baseline; raise "IN HISTORY" with a
post-layout relative offset (responsive via a --ys var) so it cap-aligns to the
top of the numerals without dragging the baseline. Rule sits at the baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:30:39 -04:00
thejayman77 2e43766d71 /onthisday: cap-align "IN HISTORY" to the top of "2013" + tighten the gap
margin-top 4→9px (text top now level with the numerals' cap) and gap 11→7px
(pulled a touch closer to 2013).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:24:49 -04:00
thejayman77 aa8ee674d5 /onthisday: year back to green, "IN HISTORY" top-aligned beside big "2013"
Per Jay: only the eyebrow was meant to go clay — the year stays GREEN to match
the date (both greens on the line tie in the green Read-more button). Layout is
now "IN HISTORY" top-aligned to the left of a large Playfair "2013", not stacked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:19:53 -04:00
thejayman77 d969810c10 /onthisday: figure-vs-photo hero, plain stacked year, clay title, more space
- Hero now adapts: photos stay full-bleed (cover); seals/logos/diagrams (PNG/SVG
  or near-square / extreme aspect) show WHOLE on a light matte (contain) instead
  of cropping to nonsense — e.g. today's SCOTUS seal.
- Year: dropped the mint chip/bubble. Now plain stacked type — "In history" over
  a larger "2013" — in the homepage card's brown family (kills the green/mint
  clash; makes the page echo the card).
- Eyebrow recolored to the homepage "On this day" card accent (#b06a45 clay).
- Added breathing room between the title and the content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 19:16:14 -04:00
thejayman77 e2e59bfdc4 /onthisday: move the year off the image into a green "In history" chip
Overlaying the year on an uncontrolled Wikimedia image was a legibility coin
flip (fine on dark photos, cluttered/unreadable on bright seals/logos like the
SCOTUS seal). Per Jay's call, lift "In history · {year}" out of the photo into a
green chip on the dateline row (year in Playfair) and drop the scrim so the hero
reads clean. Dateline wraps on narrow screens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 18:51:03 -04:00
thejayman77 783b853aee joy cards: restore eyelash title to mockup size + darken the micro-labels
- Eyelash title (On This Day / Quote of the Day) was rendering ~16px; the mockup
  is 22px (CD sized it up for prominence). Bumped to clamp(17px,2.4vw,22px).
- "The story" / "What it means" micro-labels were the mockup's light tan
  (#a89880) — low-contrast on the cream card. Darkened to #74633f for readability.
Applied to both /onthisday and /quote so the "letter" family stays consistent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 18:42:52 -04:00
thejayman77 cf018dc36d /onthisday redesign: CD's green-key "letter" with hero + year overlay
Rebuilt /onthisday to CD's On This Day design — the QOTD "letter" language in a
green key: deckle frame, "On This Day" eyelash title, a flush dateline rule, a
hero image with a left scrim and the year overlaid (Playfair), the event in
Playfair italic, a "The story" note, and a "Read more on Wikipedia" pill. Wired
to live /api/onthisday/today; hero sits on a deep-green backdrop so transparent
seal/logo images never read as broken. HubShell (bar + Back + footer + icon).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:58:48 -04:00
thejayman77 ba0838dd94 home3: rename the small-joys OTD card "A good thing today" → "On this day"
Names the ritual (this date in history) rather than describing it; matches the
/onthisday page + engine. Hero tag becomes "{year} in history" (the old
"ON THIS DAY" there was now redundant with the eyebrow).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 11:54:41 -04:00
thejayman77 50488a1885 /quote redesign: CD's "letter" QOTD (deckle frame, wax-seal date, watermark)
Rebuilt /quote to CD's QOTD v3 — a warm letter card with a dashed deckle frame,
an overlapping wax-seal date stamp, a giant faded quote-mark watermark, the quote
in Playfair Display italic, attribution, a "What it means" note, and Copy/Share.
Wired to live /api/quote/today (date → seal, text, author/work, meaning).

Uses our HubShell (HubBar + Back + footer + account icon) per the brief. Self-
hosted Playfair Display (roman+italic, OFL) for the signature quote/watermark/
seal; Newsreader for the body serif + Hanken for sans labels (kept to our stack
to avoid font sprawl). Rose accent matches CD (#a4607a family).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 11:46:28 -04:00
thejayman77 ecfc49eda0 Codex audit fixes: home3 read-time, /art OG/canonical, Bloom medallion centering
- home3 dropped source_read_minutes when mapping the brief item, so the badge
  only ever showed "1 min brief" — include it so "· ~N min full story" appears.
- /art is public but shipped the homepage title/canonical/OG (SSR-off shell, so
  svelte:head can't fix scrapers) — add /art to patch-static-heads.mjs.
- Bloom: center the circular motif in the tile's visual field (~43%) instead of
  the top illustration zone, so it reads as a medallion (per Jay + Codex).
- Polish: prefers-reduced-motion disables the tile hover-lift; fix stale
  patch-play-head.mjs comment reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 20:54:57 -04:00
thejayman77 123602dc52 Revert "/play Bloom: enlarge the flower so it fills the tile (it was already centered)"
This reverts commit 3dc72b1d31.
2026-06-25 20:47:09 -04:00
thejayman77 3dc72b1d31 /play Bloom: enlarge the flower so it fills the tile (it was already centered)
Last change recalculated to the same position (so it looked unchanged) — the
flower WAS centered, just small/floaty vs the denser word/match grids. Enlarge
the petals + ring (bigger radius) so it carries the same visual weight, still
centered above the title.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 20:45:31 -04:00
thejayman77 a74a363728 /play Bloom motif: center the flower vertically above the title (match other tiles)
Bloom used a hardcoded % anchor so it read top-heavy with a gap below. Wrap the
petals in a fixed-size .bloom-ring and flex-center it in the space above the foot
(padding-bottom clears the title) — robust, no guessed percentage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 20:41:10 -04:00
thejayman77 60a1b50376 /play polish: center motifs, fill Daily Word 2nd row, tighten bubble aim
- Motifs (Word/Word Search/Memory Match) now vertically center in the space above
  the title bar (was top-anchored with dead space); Bloom flower nudged to match.
- Daily Word: second row filled (BYTE / CALM) so it's as lively as the others.
- Bubble Blaster: aim line now starts at the shooter (no gap) and the cluster has
  a single blue target bubble matching the shooter (others recolored) so the
  matching-shot read is clear.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 20:29:34 -04:00
thejayman77 5a7c90e7be /play: bubble shooter to the right aiming a blue center ball; space under top bar
- Bubble Blaster: shooter moves right (clear of the title), a diagonal dashed aim
  line runs up-left to a now-blue bubble at the cluster center (shooter + target
  share the blue — reads like lining up a matching shot).
- Add breathing room between HubBar and the Play header (arcade-head top margin).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 20:16:48 -04:00
thejayman77 f6ae44e126 /play motifs: shrink word-search + memory-match, smaller/raised bubbles w/ angled aim
- Word Search grid: smaller fixed cells, centered, sits clear of the title.
- Memory Match: smaller tiles (capped, centered) so the motif isn't oversized.
- Bubble Blaster: smaller bubbles raised into a tighter cluster (clears the
  title), shooter a touch smaller, and the aim line is now a diagonal dashed
  line tilted up — reads like the user is aiming.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 20:05:21 -04:00
thejayman77 aa15cf119c /play arcade tweaks: Back in header (right), real word-search grid, packed bubbles
- Back moves into the header row, right-justified and level with "Play" (the
  in-game step-back row stays for select/play views).
- Word Search motif is now a real 6×5 letter grid with BYTES "found" down the
  diagonal — reads as a search, not a single highlighted word.
- Bubble Blaster motif is a packed bubble cluster + dashed aim line + loaded
  shooter (like the actual game), not an evenly-spaced grid of balls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:50:24 -04:00
thejayman77 6348835099 /play: arcade-tile hub redesign (presentation only; routing untouched)
Per CD's "Play - Arcade" + Codex's discipline note — replace just the hub
presentation; game selection/routing/URL-state/back all preserved.

- Colored game tiles, each with a motif of itself (Daily Word letter grid, Word
  Search letter rows, Bloom flower wheel, Memory Match dots), name + a calm daily
  clue on a gradient foot. Whole card tappable + a slight spring hover-lift (no
  hover-only CTA — works touch-first). Eyelash + Newsreader "Play" header.
- Bubble Blaster shown as a "Coming soon" ribbon tile (gentle bubble-pop puzzle,
  to build later); a "More coming soon" tile signals the area keeps growing.
- REMOVED from /play: the "Today's calm set" panel and the Zen Den tile. No
  dashboard/XP/streak/"played today" — Play stays pure games. Progress/achievements
  move to the opt-in /zen later (zen-reframe), done surgically.
- Fonts now match the rest of the hub (Newsreader / Hanken / Space Mono). Back
  button already present. Responsive: 3 cols → 2 (≤880) → 1 (≤560), no h-scroll.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:32:19 -04:00
thejayman77 485c4a7805 WOTD: commit cached pronunciation clip for 'beauty' (matches tracked audio)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:23:02 -04:00