S7 (a1): newsletter — THE VYNDR REPORT
Email capture + daily report assembly + operator-triggered Listmonk send.
- NewsletterCapture (dark terminal, mono) on landing (below FAQ) + /welcome
(the real signup success surface); double-opt-in note; 'Signups open soon'
when Listmonk env is unset.
- POST /api/newsletter/subscribe: public, 10/min IP limit, honeypot,
server-side email validation, forwards to 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).
- newsletterService.buildDailyReport: signals from snapshot:{sport}:latest,
STREAK WATCH via rosterLogs -> streaksService -> streakLens, THE RECORD via
ledgerService.getModelAggregate (percentage only when hit_pct != null —
n>=20 gate — else 'RECORD BUILDING · N pending'). RG footer (21+,
1-800-GAMBLER, Listmonk-native {{ UnsubscribeURL }}) in html + text.
VOICE v1.1 lint locked by tests: no '!', no banned vocabulary, numbers
only from injected pipeline data.
- sendDailyReport: creates + starts a Listmonk campaign; env-gated no-op;
refuses an empty report. Deliberately UNSCHEDULED — only
POST /api/internal/newsletter/send (internal key) triggers it.
- docs/NEWSLETTER.md: box-side Listmonk runbook (install, double-opt-in
list, API user, Coolify env, test-send).
- Spec: specs/feature-a1-s7-newsletter.md.
Tests 2398 -> 2429 (207 suites, all green); next build exit 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+29
-1
@@ -1,7 +1,35 @@
|
||||
# VYNDR — Build State
|
||||
|
||||
## Last Updated
|
||||
2026-07-10
|
||||
2026-07-11
|
||||
|
||||
## 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,
|
||||
|
||||
Reference in New Issue
Block a user