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>
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>
Per Codex — make /a/<id> feel like Upbeat Bytes has editorial judgment, not just
a summary wrapper. Trust-building, short, not an essay.
* article_summaries gains what_happened / why_matters / why_belongs (+ migration).
* summarize.explain_article: a separate, fallback-able LLM pass producing three
short notes (parsed from a labelled WHAT/MATTERS/BELONGS format). generate_summary
now stores them alongside the summary, and tops up older summaries on next view.
get_explanation returns them only when all three are present.
* API: share_page + /api/summary expose the explanation.
* share.py: renders the three-part section (accent rule) when complete; otherwise
the single "Why it's here" reason line is the calm fallback. The page polls and
swaps in both the summary and the section as they cache.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
* 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>
The bare "←" glyph rendered tiny and sat low (baseline-aligned). Swap it for a
crisp SVG arrow and lay the button out as a centered inline-flex (arrow + label),
so it reads juicier and sits properly centered in the top bar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The server-rendered /a/ summary page had no in-page way back — the only option
was the browser's back button, which feels unfinished. Add a "← Back" control on
the right of the top bar (desktop + mobile). It uses history.back() when the
reader came from within the site, and falls back to the home page for visitors
who arrived via a shared link (no useful history).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
- 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>
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>
- 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>