14 Commits

Author SHA1 Message Date
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 0c68c22221 Brand consistency: emails say "upbeatBytes" (From + digest body)
Per the brand-name standard (camelCase, one word). Updated the SMTP From default and
the digest email body/subject strings. Live env From values (auth.env + goodnews.env)
updated to match. (Web/OG brand strings in share.py + app.html are the remaining sweep.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 11:38:16 -04:00
thejayman77 2dbe73430c Sources: per-source paywall override (3-state) — fix domain-rule mis-flags
The Articles inspector revealed paywall is domain-coarse: nytimes.com is flagged,
so NY Times Learning's free Word-of-the-Day inherits 🔒 — and that flag isn't
cosmetic, it deprioritizes the content in feed sort + lead selection. Add a
per-source override so admins can correct it after inspecting.

- sources.paywall_override: NULL (domain rule) | 'free' | 'paywalled'.
- paywall.py: keep low-level is_paywalled(url) (domain); add is_paywalled_for_source
  (url, override) for the EFFECTIVE decision — never patched the domain helper
  globally (per Codex), so "domain says X" stays distinguishable from "overridden".
- Threaded everywhere ranking/UI touches paywall, via src.paywall_override on the
  shared _ARTICLE_COLUMNS + the source-aware helper: feed sort, /api/since, replace,
  lead selection, Article badge, brief composition (briefs.py), digest, source_health
  (table 🔒), the Articles inspector, and the review/attention check — so ranking and
  UI always agree.
- Endpoint POST /api/admin/sources/{id}/paywall {override}; admin UI: a select in the
  inspector header (Use domain rule / Treat as free / Treat as paywalled) + the basis
  ("ON (domain)" / "OFF (override)"), optimistic so the panel stays open.

Test: domain rule → paywalled in table+inspector+feed badge; 'free' → off in all
three; validation 422 + 404. 242 pytest + 11 vitest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 22:10:44 -04:00
thejayman77 1efe0a76eb Digest: darker section rule before 'From what you follow'
Per user — a heavier, darker divider (2px #9aa6b2) marks the brief→personal
section change, distinct from the light #e8e3d8 item separators above.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:37:33 -04:00
thejayman77 2feccedcc7 Digest: more space below the 'From what you follow' heading
Per user — separate the heading a touch more from the first followed entry
(bottom margin 18px → 28px).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:34:52 -04:00
thejayman77 71f140e8d0 Digest: restore azure on the enlarged 'From what you follow' heading
Keep the 20px bold size, bring back the brand blue (#0083ad) for differentiation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:32:41 -04:00
thejayman77 c042b947a2 Digest: enlarge 'From what you follow' heading to a proper section size
It was 11px uppercase — smaller than the 14px links, backwards for a heading.
Now 20px bold ink, clearly a section header above its items.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:23:55 -04:00
thejayman77 a7d72f2f84 Digest v2: restrained "From what you follow" section
Per Codex — give follows an immediate payoff without making the email feel
algorithmic. The editorial brief stays the star: subject + brief count unchanged.

* followed_digest_items(conn, user_id, exclude_ids, limit=3): recent items from
  the user's followed sources/tags, same accepted/non-dup/content-visible gate,
  excludes anything in the brief, capped to one per source so a single follow
  can't dominate. Empty → section omitted (no empty state in email).
* build_digest gains an optional `followed` list → a small "From what you follow"
  section AFTER the brief, only when there are items. Item rendering factored into
  shared _item_html / _item_text_lines helpers.
* send_due_digests computes the followed items per user (excluding the brief).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:17:17 -04:00
thejayman77 69ed202c4e Digest masthead: use the real logo as a hosted PNG
Per Codex — cleanest, most brand-consistent option. Rasterized logo.svg to a
small transparent logo-email.png (360px wide, shown at 180px for retina), served
at /logo-email.png. alt="Upbeat Bytes" keeps the brand if a client blocks
images. SVG isn't email-safe; a hosted PNG is the standard approach.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 17:24:43 -04:00
thejayman77 798f08b256 Digest masthead: two-tone tight-tracked 'upbeat bytes' wordmark
Per user — make the text masthead read as the brand: lowercase 'upbeat bytes'
with a space but tight letter-spacing (-0.045em, not standard), in the logo's
two colours (azure #0083ad + navy #002772). Closest email-safe echo of the logo
short of a hosted PNG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 17:15:31 -04:00
thejayman77 bd253b4a9c Digest email: lowercase 'upbeat bytes' masthead + intro spacing/divider
Per user: lowercase masthead to echo the logo's character (kept as text, not an
image — email clients strip SVG and block remote images, so text always renders
and degrades gracefully). More breathing room before the 'Good morning' intro,
and a horizontal divider after it to match the item separators. (Real logo PNG
remains an option if wanted.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 17:03:56 -04:00
thejayman77 7da14bd4fd Digest email: 'Daily Highlights' masthead + warm intro, point back to the site
Per user feedback: rename from 'Today's good news' (which implied the site was
done for the day) to a publication-style 'Upbeat Bytes — Daily Highlights'
masthead with a warm morning intro and a sign-off that links back to the site
('more good news is always waiting'). Adds 'why it's here' to the plain-text
part too. No images by design (lightweight, mail-client-safe).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 16:55:17 -04:00
thejayman77 1956d7fd23 Digest polish: honest on-site wording, one-tap opt-in after sign-in, List-Unsubscribe
* On-site end-cap now says "You're caught up for now." — honest, since Highlights
  refreshes through the day (the email keeps the daily "see you tomorrow").
* Anonymous "Get tomorrow's brief by email" now honors the one-tap promise:
  sets a pending flag, opens sign-in, and auto-enables once auth resolves.
* Email compliance (RFC 2369/8058): send_email takes optional headers; the digest
  sets List-Unsubscribe + List-Unsubscribe-Post=One-Click, and a POST
  /api/digest/unsubscribe handles native one-click (GET still serves the page).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 16:35:05 -04:00
thejayman77 cf5cbb33c0 Daily digest (opt-in) + finite "you're caught up" ending
Reader-retention as ritual, not capture (Codex's framing). Opt-in calm morning
email of today's brief; the on-site twin is the finite end-of-feed nudge.

* Schema: users.digest_enabled + digest_unsub_token; digest_sends (dedupe +
  visibility). auth.get_user now returns the digest fields.
* goodnews/digest.py: build (dated calm subject, items w/ summary + "why it's
  here" + UB/source links + one-click unsubscribe, "you're caught up" sign-off)
  and send_due_digests (morning-window gated, >=4-item floor or skip quietly,
  deduped, reuses SMTP). No streaks/urgency/"you missed".
* API: /auth/me exposes digest_enabled; POST /api/account/digest toggle;
  GET /api/digest/unsubscribe (token, no login, calm confirmation page).
* CLI: cycle gains a morning-gated digest step (--no-digest) + a send-digests
  command (--force).
* Frontend: digest toggle on the Account profile; the Highlights end-cap now
  says "you're caught up — see you tomorrow" with a one-tap "Get tomorrow's
  brief by email" (signed-in → enable; anon → sign in).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 16:17:46 -04:00