Commit Graph

13 Commits

Author SHA1 Message Date
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