Merge S7 (a1): newsletter — THE VYNDR REPORT

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

# Conflicts:
#	BUILD-STATE.md
#	CLAUDE.md
This commit is contained in:
Kev
2026-07-11 14:32:18 -04:00
14 changed files with 1297 additions and 0 deletions
+27
View File
@@ -19,6 +19,33 @@ Zero out-of-pocket; everything config-flip-ready but DISABLED/organic.
migration in docs/PARTNERS.md adds `user_profiles.partner_ref`; NOT run).
- **docs/PARTNERS.md** — ref link spec, Stripe promo-code mapping convention
(partner code == promotion code), TODO migration SQL.
## Session S7 (a1 board, 2026-07-11) — Newsletter: THE VYNDR REPORT ✅
Branch off day1/a1-board (e4d2e79). 2398 → **2429 tests** (207 suites),
web build exit 0. Spec: `specs/feature-a1-s7-newsletter.md`. Runbook:
`docs/NEWSLETTER.md`.
- **Capture**: `NewsletterCapture` (dark terminal, mono data) on the landing
(below FAQ) + `/welcome` (the real signup success surface — the signup
"done" card auto-redirects in 1.5s, useless for a form). Double-opt-in
note under the field; "Signups open soon" when Listmonk env is unset.
- **Backend**: `POST /api/newsletter/subscribe` (public, 10/min IP limit,
honeypot, server-side email validation) → Listmonk subscribers API with
`preconfirm_subscriptions:false` (Listmonk sends the confirmation).
No env → calm 200 `{ ok:false, reason:'not configured' }`. Next proxy
`web/src/app/api/newsletter/subscribe/route.ts` (S25 rule).
- **Assembly**: `newsletterService.buildDailyReport` — signals from
`snapshot:{sport}:latest`, STREAK WATCH via rosterLogs → streaksService →
streakLens (one row per player), THE RECORD via `getModelAggregate`
(percentage ONLY when hit_pct != null — the n≥20 gate — else
"RECORD BUILDING · N pending"). RG footer (21+, 1-800-GAMBLER,
`{{ UnsubscribeURL }}`) in html + text. VOICE lint locked by tests:
no `!`, no banned vocabulary, numbers only from injected pipeline data.
- **Send**: `sendDailyReport` creates + starts a Listmonk campaign;
operator-triggered ONLY via `POST /api/internal/newsletter/send`
(internal key). NOT scheduled. Empty report (0 signals + 0 streaks)
refuses to send.
- **Env for Coolify** (all four required, else graceful no-op):
`LISTMONK_URL`, `LISTMONK_USER`, `LISTMONK_TOKEN`, `LISTMONK_LIST_ID`.
## Current Phase
SHIP BUILD v59.0 — Overnight session: ledger team/opponent addendum,