276 Commits

Author SHA1 Message Date
thejayman77 cf65243e07 zen: rebuild UB locomotion — real 3D steering + motionRoot/visualRig split (Codex)
Not a tuning pass — a locomotion rebuild per Codex's review. Replaces the X-axis shuttle +
scripted 180° U-turn with a proper 3D steering controller (behavior.js): UB seeks wandering
waypoints through the whole tank (XYZ incl. near/far depth passes), eases speed via limited
accel, steers with a rate-limited quaternion toward its velocity, banks into curves, and
softly veers away from walls BEFORE reaching them — no scripted turns.

Architecture: motionRoot (Group) owns world position + heading + bank; the visual rig plays
in-place body clips inside it, so navigation never fights the skeleton and the controller is
the sole heading authority (clips carry no root motion — verified). Fuller clip set exported
(idle/cruise/fast/turn{L,R}{in,loop,out}/eat*, build-clips.mjs). Authored clips play at their
own timing; only cruise cadence scales with speed. Debug panel: cruise speed + roam W/H/DEPTH
+ liveliness, a live readout (mode/clip/speed/turn/pos), and the raw-clip preview.

Next (Codex step 3): needs-driven personality — forage/zoomies/surface/inspect — on this base.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:53:35 -04:00
thejayman77 45bd44834e zen: fluid rework — slow banked U-turns synced to the clip + calmer cruise + clip preview
Phase-B first pass was stiff/awkward — the turn whipped ~180° in ~0.7s while the turn clip
ran 2.5s (fighting), and a flat fish spun that fast went edge-on. Rework:
- U-turn now runs over the clip's duration (~2.4s), smootherstep ease-in-out, banks in/out
  (roll), glides the arc instead of stopping — heading + animation move together.
- Calmer: slower cruise (0.26), fewer modes (cruise/rest, dropped jerky darts), longer
  timers, gentle continuous roll/pitch body sway so cruising isn't rigid.
- New "preview clip" tuner control: freeze locomotion + loop any raw clip broadside — proves
  the animation itself is fluid (isolates engine vs asset).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:33:29 -04:00
thejayman77 d13811319d zen: Phase B — UB swims with a behavior engine (wander, cruise/rest/burst, U-turns)
UB is no longer a static in-place loop. New behavior.js owns locomotion: UB wanders a
bounded tank, cruises at a chosen speed, drifts to new depths (nose tilts into it),
occasionally rests or darts, and banks through smooth U-turns at the edges — the tail
beats faster/slower with speed. All clips are in-place, so the engine drives world
position + heading and crossfades between the named clips (idle/cruise/burst/turnL/turnR).

Multi-clip GLB built via tools/glb-split/build-clips.mjs (5 clips, 8.7MB — orphaned Take
accessors explicitly disposed). aquarium.js reworked: clip crossfade + per-frame behavior
apply. Tuner (/zen?debug=1) now exposes scale + Behavior (cruise speed / roam width /
roam height / liveliness) + the fins section. Reduced-motion calms speed + liveliness.
Still admin-gated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 20:38:43 -04:00
thejayman77 9365f69b4a zen: calmer UB — live speed control (default 0.7×) + a touch bigger (scale 1.2)
Swim1_norm read a bit restless; add a tunable playback speed (default 0.7× for a calm
glide, ×0.7 again under reduced-motion) wired into the render tuner, and bump the default
scale to 1.2 for more presence. Both live-adjustable at /zen?debug=1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 19:37:20 -04:00
thejayman77 a47897e7b1 zen: UB base loop → Swim1_norm (gentle in-place swim, not the static Idle)
Idle is a resting pose (only eye/mouth/fin micro-motion). Swapped the base loop to
Swim1_norm — a ~2.5s swim cycle with ZERO root drift (verified via trim-clip.mjs), so
UB undulates continuously without traveling off-screen. Generalized the trimmer
(tools/glb-split/trim-clip.mjs: extract any [start,end] range, rebased to 0, + drift report).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 19:21:55 -04:00
thejayman77 ce69b8cd18 zen: UB is now the Queen angelfish (real model) + fix admin lockout
- Admin lockout: /zen checked blockedForViewer() before auth loaded, so a hard-refresh/
  direct-link bounced admins to /play. Now revalidate auth (await refresh if !ready)
  BEFORE the gate check.
- UB swap: retired the two-tail koi (ub.glb/ub-split.glb) for the vetted Queen angelfish.
  Trimmed the 75.67s baked Take down to just the Idle loop (tools/glb-split/trim-idle.mjs
  → 16MB → 6.9MB) → static/models/ub-angelfish.glb. aquarium.js reworked for the pack's
  ONE-mesh/TWO-material layout (…_body opaque single-sided; …_fins opaque alpha-tested,
  tunable); animation is the trimmed Idle. Debug tuner (/zen?debug=1) updated: yaw/pitch/
  scale + one fins&tail section. Still devgate IN_DEV={'zen'} — admin-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:10:38 -04:00
thejayman77 e64c5ced3c art: verify pointerId in dragMove + clear activePointer on every exit (Codex final)
dragMove now ignores events from any pointer other than the one that started the drag
(if !dragging || e.pointerId !== activePointer), dragStart ignores a second pointer
mid-drag, and activePointer is reset in enterZoom/fit/Escape (the close effect already
did). Prevents a second/hybrid-device pointer from hijacking an active drag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:00:12 -04:00
thejayman77 c42f29537b art: harden zoom pointer lifecycle (Codex sign-off)
- Reset dragging on every exit path (enterZoom, fit, Escape, lightbox-close effect) so a
  drag interrupted by Escape/Fit can't carry the grabbing state into the next session.
- Drag ends on pointerup/pointercancel/lostpointercapture (dropped pointerleave, which
  fought the capture) so a drag genuinely continues outside the image.
- dragStart guards e.button===0; track the captured pointerId and release only when
  hasPointerCapture() — no double-release throws.
- a11y: slider aria-valuetext ("150 percent").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:45:03 -04:00
thejayman77 494e9dfcdd art: drag-to-pan the zoomed artwork (persistent position; zoom holds the spot)
Per request: the inspector now pans only while the mouse button is held (grab/grabbing),
using a persistent translate rather than cursor-follow — so you place a detail where you
want it and it stays put. Zooming (slider/±/arrows) scales the translate by the same
ratio, keeping the viewport-centred spot fixed so you can keep magnifying that exact area
without it recentering. Pan is clamped to the image bounds (pointer-capture drag); 1× recenters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:20:12 -04:00
thejayman77 6b2458f674 art: variable gallery-inspector zoom (1×–4× toolbar) instead of a binary jump (Codex)
Reworked the lightbox desktop zoom from a fixed 2.5× toggle into a proper inspector:
enter at 1.5×, a quiet floating toolbar (− / slider / + / % / Fit) drives a continuous
1×–4× scale in 0.1 steps, cursor movement keeps panning (transform-origin). Fit returns
to the framed gallery view; Escape steps out then closes; the slider takes native arrow
keys. Removed click-to-exit on the artwork (too easy to trigger while inspecting) — exit
is the visible Fit control or Escape. Toolbar is a translucent dark pill, hidden on touch
(native pinch). Zoom resets when the lightbox closes. Uses the cached full-res asset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:13:00 -04:00
thejayman77 f3005e626d art: desktop zoom in the lightbox (pan-by-cursor), mobile keeps pinch
The full-screen lightbox showed the framed piece capped at ~66vh, so on desktop it was
barely larger than the page view and there was no way to inspect detail (mobile can pinch).
Add a "Zoom in" affordance: it swaps to a magnified inspection view (full-res image scaled
2.5×) where moving the cursor pans via transform-origin; click or Escape steps back to the
framed view, Escape/✕/backdrop close. Restructured the lightbox from a single <button> to a
dialog (backdrop button + close button + stage) so the controls are valid/accessible. Zoom
button hidden on touch (hover:none) — native pinch covers mobile. Uses the already-cached
full-res copy (/api/art/image/<id>?size=full); fade-in, frame/thickness, rotate-on-portrait
all preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:52:10 -04:00
thejayman77 4d2f5e3703 home: hero image as <img referrerpolicy=no-referrer> (privacy consistency, Codex)
The homepage hero was a CSS background-image, the one image on the site that couldn't
carry referrerpolicy — so a remote hero leaked the referrer to the publisher CDN while
article cards + share pages suppressed it. Replace with a real <img referrerpolicy=
"no-referrer">; the retry probe now sets probe.referrerPolicy='no-referrer' too. object-fit
cover/contain replaces background-size (contain keeps the matted framed-plate look via
padding), fixed 5/4 footprint, fade-in and typographic fallback preserved; img onerror
falls back to the typo cover post-reveal. (Suppresses the referrer, not the IP — zero
third-party requests still requires policy 'none' or local caching.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:38:05 -04:00
thejayman77 2dc4419024 images/analytics: purge on policy revoke + engagement warm-up note (Codex close-out)
- newsimg.purge_source(): when a source leaves 'cache' (permission revoked / re-classified),
  the admin image-policy endpoint now deletes that source's re-hosted copies immediately,
  rather than leaving them inaccessible-but-on-disk. Endpoint returns {purged}.
- Admin "Engaged readers" carries a warm-up note: tracking began 2026-06-30, so low
  rolling windows are partly warm-up, not all bots (compare d7 after a week, the window
  after its full span). Guards against misreading "6 engaged vs 135 visits" as 129 bots.
Tests: purge_source removes only the target source's copies; endpoint reports purged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:29:55 -04:00
thejayman77 f416e13700 analytics: honest engagement metric — Engaged readers vs Recorded visits (Codex)
Admin now shows two numbers:
- Recorded visits: the existing raw count (one daily 'visit' beacon; still includes
  UA-spoofing bots that slip past the UA filter).
- Engaged readers: distinct visitor-day with DELIBERATE activity — either the new
  gesture-gated 'engaged' beacon (fires once/day only after ~8s visible AND a real
  scroll/pointer/key/touch) or a deliberate action (source_click, full_story, share,
  replace_used, paywall_replace, not_today/less_like_this/hide_topic, game start/
  complete/share). Explicitly EXCLUDES auto-fired visit/summary_viewed/open, replace_none,
  and game *_arrival (a share-loop landing, not engagement).

armEngaged() in analytics.js (wired in the global layout) + a mirrored vanilla-JS beacon
on the server-rendered /a/<id> share pages. 'engaged' added to the event allowlist and
fired with article_id=0 so the uniqueness constraint dedups it per day. queries.admin_stats
gains engaged_today/d7/d30. Bots are doubly excluded (UA filter at the beacon + the
gesture gate). Tests cover the metric (engaged + deliberate counted; visit/summary/arrival
not). 447 backend + 36 frontend tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:07:24 -04:00
thejayman77 8a7606e20d images: fix two fetcher bugs + add source-level image-rights policy (Codex)
Fetcher (the two remaining bugs Codex found):
- Real redirects are now followed. _NoRedirect makes urllib RAISE HTTPError on 3xx, so
  the old status-branch was dead code (mocked tests masked it). Handle 301/302/303/307/308
  HTTPError as redirects (re-validate the destination); classify 4xx≠429 as PERMANENT
  (negative-cached), 429/5xx/network as transient. Real-opener redirect + 404/5xx tests.
- The megapixel ceiling is now enforced: explicit `w*h > _MAX_PIXELS` check BEFORE load()
  (Pillow only warns at MAX_IMAGE_PIXELS). Test with a lowered ceiling.

Image-rights policy (per Codex + owner decision — only cache what's cleared):
- sources.image_policy: 'cache' (re-host a downscaled copy — license/permission/PD only),
  'remote' (hotlink the publisher's image — the conservative DEFAULT), 'none' (no image).
- newsimg.display_url resolves the display URL per policy; applied in Article.from_row so
  feed/brief/history return the right URL, and in share.py (og/twitter still reference the
  publisher's own image, never re-hosted). warm() + /api/img both gated on 'cache'.
- Frontend uses the server-resolved image_url (reverted the hardcoded /api/img); the
  graceful retry covers remote hotlinks too. Admin: per-source image-policy selector +
  POST /api/admin/sources/{id}/image-policy. Default 'remote' → nothing re-hosted until
  a source is explicitly cleared.

445 backend + 36 frontend tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:01:11 -04:00
thejayman77 86d9897113 ui: reserve the scrollbar gutter so the top bar stops shifting between pages
Pages tall enough to scroll showed a ~15px scrollbar; short pages didn't — so the
centered top bar jumped left/right as you navigated. scrollbar-gutter: stable on html
(SPA app.css + the server-rendered share pages) keeps the layout width constant. No-op
on overlay-scrollbar platforms (mobile), which never shifted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 04:52:59 -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 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 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 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 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