Path-only @junk matcher on upbeatbytes.com (*.php, /wp-*, /.env, /.git, /phpmyadmin,
/vendor, etc.) returns 403 instead of falling through try_files to a 200 SPA shell.
Never matches by User-Agent, so real users + Googlebot/Bing are untouched. Applied to
the live Caddyfile (validated + reloaded) and mirrored into the repo snapshot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
`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>
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>
- Admin joy item route moved to /api/admin/joys/{kind}/items/{item_id} so the
/add and /repick verbs resolve to their own routes instead of 422-ing as a
non-int item id (the launch blocker). Frontend mutate URL updated to match.
- Re-pick now excludes the currently-shown item: the endpoint reads today's
daily pool_id and passes it as `avoid`, so "Re-pick today" yields a different
item. Added `avoid` to pick_daily/_candidates across wotd/quote/onthisday.
- WOTD sense selection: the LLM now proposes word + intended part of speech, and
_lookup prefers that sense (fixes "serene" returning the archaic noun).
- On This Day tone prompt tightened to favor genuinely uplifting events and
exclude merely procedural/political-administrative ones.
- Caddy @hidden now also noindexes /word /quote /onthisday /admin (+ .html).
- Regression tests: add/repick resolve (401 not 422), add/feature/block/delete,
re-pick excludes current; WOTD pos-preference + proposal parsing units.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Self-hosted Manrope (OFL) as the hub sans; nav lighter (weight 500, soft slate, not all
"on"). Logo up to 58px.
- News card: photo on top + headline below, and it now respects the reader's saved
Closer-to-Home filter (goodnews:home/homeScope) so the headline matches their Brief.
- Art card: rectangular cover-cropped thumbnail on the LEFT (crops ragged scan edges),
text on the right — variety against the photo-top news card.
- Play/Daily Moment: tinted backgrounds, bigger centered icon+title, blurb left-aligned.
- /fonts/* + /textures/* served immutable (Caddy live + snapshot).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- /api/art/image/{id} now answers HEAD as well as GET (was 404 on HEAD) — mirrors the
/a/{id} fix. Added tests/test_art_api.py (GET+HEAD+size=full fallback + today payload).
- /textures/* served immutable (long cache) instead of no-cache; excluded from the
revalidate matcher. Live Caddyfile + repo snapshot both updated.
- Lightbox: Escape closes it, and focus moves to it on open (keyboard-friendly).
- Trimmed the gallery's top padding so "Daily Art" sits closer to the bar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sharpen the existing daily-game share loop into something measurable (per Codex's
"instrument what you have, then feed people into it" plan), ahead of a Show HN launch.
Analytics:
- Per-game funnel events <game>_{arrival,started,completed,shared} (article_id=0).
arrival = landed via a shared link (utm_source=game_share); started = first move
(guess/find/flip); completed = solved/cleared/Full Bloom; shared = on share success.
- trackVisit() moved into the global layout so direct /play landings count; the
server-rendered /a/ share page now creates a visitor token + sends a daily visit
beacon (first-time /a/-only visitors were previously dropped).
- Admin "Games funnel" panel: arrivals / engaged / completed / shared, per game.
Sharing:
- Memory Match gains a Share button (it was the only game without one).
- All shares deep-link to the exact game+variant with a full https:// URL +
utm_source=game_share (gameShareUrl helper), instead of a bare /play.
- "shared" is counted only after navigator.share()/clipboard.writeText() succeeds.
/play social metadata:
- /play served homepage canonical/OG (static SPA, ssr=false). postbuild script
patches build/play.html's head to /play canonical/title/description/OG; fails the
build if the homepage tags drift. Caddy try_files now serves {path}.html so /play
is served from the patched file (snapshot in deploy/caddy/).
Tests: backend 352, frontend 27.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Post-deploy slow-load fix (telemetry-confirmed): boot-slow beacons showed the
shell arriving fast (33-79ms) but freshly-deployed chunks taking 3-5s, every
event within ~6-8min of a deploy, the same chunks fast HITs later. Cause: the
new shell went live pointing at chunk hashes not yet warm at the edge, so the
first visitor fetched them cold from the residential origin (modulepreload
fires them together → one unlucky "chunk warmer").
Reorder sync-static.sh: warm the immutable chunks at the edge BEFORE swapping in
the new shell, so a published shell never references cold chunks. Shell + routes
still warmed after publish. Pure deploy-script change — no runtime/SW changes.
Warms the origin's nearest POP (covers local users + our own post-deploy
testing); a distant POP still cold-fills once (inherent to a residential origin).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* client_error details, not just a count: new client_errors table + POST
/api/client-error (reason/path/user-agent/time) + GET /api/admin/client-errors.
The boot-seatbelt beacon now sends the reason + path (once per page); the admin
Overview lists the recent errors so we can tell chunk vs SW vs API vs JS — the
truth meter for the next day as the new SW propagates.
* Deploy warming now also hits the shell, routes (/play /account /admin), SW,
version.json, word lists, and icons/logo/font — not just immutable chunks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The post-deploy blank/slow load: new hashed chunks weren't in Cloudflare yet, so
the first visitor pulled them cold from the residential origin — AND the service
worker simultaneously precached ~30 of those cold assets (a request storm),
pushing past the 7s boot timeout.
* sync-static.sh now warms the CF edge cache (fetches every immutable asset
through the public domain) so the first visitor gets HITs, not cold-origin.
* Service worker no longer bulk-precaches on install (the browser already caches
immutable assets for a year); it caches the shell + assets lazily as used. No
more storm.
* Boot-recovery timeout 7s → 10s so a merely-slow load doesn't flash the card.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per Codex. Shared deploy/sync-static.sh used by both publish scripts: sync new
hashed chunks first WITHOUT pruning old ones (grace window so in-flight/old
clients keep chunks they still need), then other assets, then index.html, then
service-worker.js last — so a new shell never appears before its chunks exist.
Old immutable chunks pruned after 14 days to bound disk growth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per Codex — guard against the static logo silently going missing (which would
break the newsletter masthead). Non-fatal curl check after Caddy reload.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds + rsyncs the static site without the API container rebuild/Caddy reload
that publish.sh does — for quick UI/CSS/copy iteration. Use publish.sh when
backend changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One command to rebuild the frontend, sync it to the live Caddy site, refresh the
API container, and reload Caddy. README documents the upbeatbytes.com topology.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- deploy/goodnews.service: oneshot unit running 'goodnews cycle' with a
generous TimeoutStartSec so long classify runs are not killed.
- deploy/goodnews.timer: every 15 min, Persistent=true to catch missed runs.
- deploy/goodnews.env.example: LLM endpoint + DB path for the scheduled run.
- README: scheduling/install docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>