Feature/treaty dashboard message first#75
Conversation
Two new triggered emails fire after a YES vote on the 1% Treaty referendum (`TREATY_REFERENDUM_SLUG`): 1. `post-vote-share-email` to the voter. Body is the canonical "I love you and don't want you to suffer and die of horrible diseases" share message verbatim, with the voter's referral URL inline + a single "End war and disease" button. Designed to be forwarded as-is: the user hits Forward, pastes two addresses, sends. The email IS the share kit. Deduped on `voteId` so re-votes don't double-send. 2. `referral-first-conversion-email` to the referrer (when `vote.referredByUserId` is set). Sent EXACTLY ONCE per referrer — the moment of their first confirmed conversion. Per-vote pings get spammy fast and don't add signal after the first confirmation. Subsequent conversions live on /dashboard as ambient stats. Deduped on `referrerUserId`. Both emails lead with the chain math (`32 doubling rounds × 2 referrals each = 4,300,000,000 humans`) so the user internalises the multiplier and communicates it correctly to the people they share with. A user who understands the math is a better evangelist than one who doesn't. Both use `scope: "onboarding"` so users can opt out via the existing unsubscribe rails. Failure logging is best-effort (`log.error`) — email send failure doesn't break the vote write. TODO.md notes the deferred work: - Monthly chain-stats digest (cron + recursive CTE; only send when N > 0 to skip depressing zero-count months). - Email-template screenshots in the visual review pipeline so reviewers can see email copy without setting up Resend locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "I love you and don't want you to suffer and die of horrible
diseases..." message was duplicated across three surfaces (the
DashboardShareCard, the post-vote share email body, the referral
first-conversion email's tone). Pulled into a single helper —
`lib/share-message.ts::buildShareMessage(referralUrl)` — so the
canonical wording lives in one place. Any future edit lands once
and propagates everywhere.
Added `lib/email/share-footer.ts` rendering a compact divider +
canonical-message + chain-math block. Designed to drop in at the
bottom of any email going to an authenticated user who has a
referral URL, so the share kit is always one paste away no matter
which email they received.
Wired into:
- DashboardShareCard: replaces the inline `buildDefaultMessage`
- post-vote-share-email: `buildPostVoteShareMessageText` now
delegates to the shared helper
- referral-first-conversion-email: appends a ShareFooter to both
HTML and plain-text bodies; new `referrerReferralUrl` input
that the vote route now fetches alongside the referrer's email
Deferred (TODO):
- Retrofit ShareFooter onto pre-existing engaged-user email
templates (task-assignment-notification, task-comment-notification).
Sweeping change across templates; better as its own focused PR.
- The monthly chain-stats digest is still its own work: cron +
transitive-chain recursive CTE + variant copy for the
zero-conversion month (resend the forward kit, NOT silent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nd kit
Earlier note said to skip zero-conversion months as "silent is better than
depressing." Reversed — silence treats zero conversions as a user-failure
signal when it's actually a we-failed-to-activate signal. The user with
zero conversions is exactly who needs the nudge; the ones already converting
are self-motivated.
New rule: monthly email until unsubscribe, two variants picked by N:
- N > 0: pure positive reinforcement (stats + chain math)
- N == 0: resend the forward kit ("Still 30 seconds. Still two humans.")
Also captures the ShareFooter retrofit follow-up onto task-assignment and
task-comment-notification templates (engaged-user emails that already
ship via sendResendEmail and would benefit from the share kit at the
bottom).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mails
Wires `buildShareFooterHtml`/`Text` into the two existing engaged-user
email templates. Both go through `sendResendEmail` to recipients with a
`recipientUserId`, so we can fetch their handle+referralCode and build
the personal referral URL the share footer needs.
When the recipient is external (no `userId` — a leader's office, a
press contact), `recipientReferralUrl` is null and no share footer
renders. That keeps outreach emails to non-voters off-brand-free.
Code path:
notify*Notifications.server.ts → getRecipientReferralUrl(userId)
→ fetches User.referralCode + person.handle → buildUserReferralUrl()
→ passes to build*Email() → buildShareFooterHtml/Text() append
131 existing email tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New Playwright spec `e2e/email-screenshots.spec.ts` renders each
outbound email template with representative sample tokens, sets the
HTML as page content at a 640px-wide email-client viewport, and saves
the screenshot to `screenshots/{project}/email-{name}-{project}.png` —
the same shape route screenshots use, so `build-visual-review.mjs`
picks them up automatically alongside page screenshots.
Coverage:
- email-magic-link (War on Disease theme)
- email-post-vote-share
- email-referral-first-conversion
- email-task-assignment (with ShareFooter)
- email-task-comment-notification (with ShareFooter)
Reviewers no longer have to set up Resend locally and mail themselves
a test to see what outbound copy actually looks like.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #71 (feature/managed-task-tree-sync) is on main. Update TODO.md status lines accordingly. Mark sitemap + plaintiff damages items shipped. Drop ShareableSnippet.updatedAt — unused by consumers; cuts noise in this generated catalog. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cron at \`0 14 1 * *\` UTC (1st of every month at 14:00 UTC) calls
\`/api/cron/monthly-chain-digest\` which iterates every YES treaty
voter with an email and sends one of two variants based on the
voter's direct conversion count over the past 30 days:
- N > 0: positive-reinforcement digest. Subject names the count +
month. Body shows monthly + all-time totals, the doubling-rounds
math (32 rounds × 2 each = 4.3B), a dashboard link, and the
canonical ShareFooter so the user can paste the share message
into any channel.
- N == 0: re-send the forward kit. Subject pivots to "Still 30
seconds. Still two humans you love." Body is the canonical
share message verbatim. The zero-conversion user is exactly who
needs the nudge; silence treats it as user-failure when it's a
we-failed-to-activate signal.
Deduped via \`EmailLog.dedupeKey = monthly-chain-digest:{userId}:{yyyy-mm}\`
so the cron is idempotent — multiple invocations during the same
calendar month never double-send.
The current monthly count is just direct conversions
(\`referredByUserId = userId\`). A future enhancement can swap this
for a transitive recursive CTE to surface the full chain size + which
doubling round of 32 the user is actually on.
Adds the two variants to email-screenshots.spec.ts so reviewers see
both versions in the visual review.
8 vitest cases for the builders, all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Mark EmailLog FAILED when sendResendEmail returns a non-sent result
(disabled / suppressed / etc.). Previously these left the row stuck in
QUEUED forever; matches the pattern in task-notifications.server.ts
(line 616).
- Fix referralUrl docstring on post-vote-share — the comment claimed
`warondisease.org/r/ABCD or /@handle` but buildUserReferralUrl actually
produces `${baseUrl}/vote/${handle | referralCode}`.
- "Live conversion counts live on your dashboard" -> "Live conversion
counts are on your dashboard" (double-live). Both HTML and text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughRemoves ChangesShareable snippet shape
Treaty UI & Dashboard
CI & Visual Review
Playwright & e2e
Share message & DashboardShareCard
Email system: templates, sender, publisher, cron
Task notification wiring
Tests & Docs
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Pull request overview
This PR adjusts the Treaty dashboard to prioritize the share message UI, updates campaign TODO tracking, and reduces churn in the generated @optimitron/data/parameters snippet exports by removing the updatedAt field.
Changes:
- Removes the Treaty dashboard’s
ReferralLinkBanner(and the associated session refresh/state) so the share-message card is the first primary surface. - Updates
TODO.mdto reflect recent merges and to convert some funnel items into checkbox-tracked tasks. - Removes
updatedAtfromShareableSnippetand from the generatedshareableSnippetsentries.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| TODO.md | Updates campaign snapshot + converts select funnel tasks into checkbox format. |
| packages/web/src/components/site/TreatyTaskDashboardClient.tsx | Removes ReferralLinkBanner and related state/hooks to keep the share-message card first. |
| packages/data/src/parameters/parameters-calculations-citations.ts | Drops updatedAt from the exported snippet type/data. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
- **Privacy bug**: voter displayName was leaked to the referrer email
unconditionally, even when the voter's Person.isPublic is false
(default). Referral links can be shared anywhere, so the "referrer"
may not actually know the voter. Now gate on `voter.person.isPublic`
— if false, the email uses "A new voter".
- **Display Identity rule**: vote/route.ts hand-rolled
`person: { select: { displayName: true, handle: true } }` and read
`voter.person?.displayName` directly. Now spreads `userDisplaySelect`
and reads via `getUserDisplayName(voter)`, per CLAUDE.md.
- **DRY**: extract `sendDedupedEmail` server helper. Both senders had
~30 lines of identical claim/send/mark scaffolding. The helper owns
it; each sender drops to ~10 lines (template id + dedupe key + body
builders).
- Drop the passthrough `buildPostVoteShareMessageText` wrapper (inline
`buildShareMessage` directly) and move its content tests to a
share-message.test.ts where they belong.
- Drop low-signal constant-equality tests (`TEMPLATE_ID === "..."`,
`SUBJECT === "..."`). They restate the constant declaration and only
fail when someone intentionally renames.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Side-by-side screenshots in the visual review report were rendered at the container width, which on a wide grid leaves each shot too small to read small UI text. Click any image to open it full-viewport (max-width / max-height: 100% with object-fit: contain). Click the image again to toggle 1:1 native-pixel zoom (scrolls inside the backdrop). Click the backdrop, the Close button, or press Esc to dismiss. Pure inline CSS + vanilla JS. No new dependencies and no runtime impact on the served pages — only the static review HTML. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- **Decouple voter share + referrer first-conversion sends** (vote/route.ts). Previously both lived inside a single try/catch, so a failure in voter fetch/send suppressed the referrer's email. Independent try blocks; each side logs its own error. - **Monthly digest month-label mismatch** (monthly-chain-digest.server.ts). The 30-day window covers the prior calendar month when cron fires on day 1, but the label used monthLabel(now) — so an April-data email said "May 2026". Now labels from windowStart; dedupe bucket still keyed on now for the one-per-calendar-month guarantee. - **Monthly digest EmailLog stuck in QUEUED on non-sent SendResult**. Switched the local send wrapper to the shared sendDedupedEmail helper, which marks disabled/suppressed results as FAILED with send_aborted:<status>. Also rolls non-sent statuses into the publisher's failed counter so the cron summary surfaces them. - **Extract getRecipientReferralUrl** to lib/referral-url-helpers.server.ts. The function lived verbatim in both task-assignment-notifications and task-comment-notifications; CLAUDE.md "Delete on sight: copy-paste". Both callers import the shared helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LiveCounter rendered a real ticking value during the visual-regression spec, so every screenshot capture saw a different millisecond of the counter and Argos surfaced false-positive diffs on every page that embeds it (/employees, /presidents, signer rows, dashboards). The spec already supports `data-visual-mask="dynamic"` + the placeholder attribute — death-counter and money-counter both use it correctly. This component had a non-standard `data-volatile="..."` attribute that the spec doesn't recognize, so the mask never applied. Match the established pattern so screenshots capture a stable placeholder (`123,456` / `$123,456,789,012`) instead of live values. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous setup used `actions/upload-pages-artifact` + `actions/deploy-pages`, which atomically replaces the entire Pages site each deployment. Combined with the workflow's `rm -rf "$pages_root"`, every PR's CI run wiped every other PR's visual review URL — clicking the "Visual review" check on an older PR returned 404 the moment any other PR's CI finished. Switch to `peaceiris/actions-gh-pages@v4` with `keep_files: true`, writing to a long-lived `gh-pages` branch. Each PR's content lands at `pr-<N>/<short_sha>/` (the URL the commit status posts) AND at `pr-<N>/latest/` (a stable per-PR link). Other PRs' directories on the branch are preserved. The job's `contents` permission moves from `read` to `write` so the action can push to gh-pages. `pages: write` / `id-token: write` are removed — they were only needed by the old deploy-pages flow. One-time repo setup (manual): Settings → Pages → Source must be set to "Deploy from a branch" with branch `gh-pages`. The action creates the branch on its first successful run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LiveCounter rendered a real ticking value during the visual-regression spec, so every screenshot capture saw a different millisecond of the counter and Argos surfaced false-positive diffs on every page that embeds it (/employees, /presidents, signer rows, dashboards). The spec already supports `data-visual-mask="dynamic"` + the placeholder attribute — death-counter and money-counter both use it correctly. This component had a non-standard `data-volatile="..."` attribute that the spec doesn't recognize, so the mask never applied. Match the established pattern so screenshots capture a stable placeholder (`123,456` / `$123,456,789,012`) instead of live values. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous setup used `actions/upload-pages-artifact` + `actions/deploy-pages`, which atomically replaces the entire Pages site each deployment. Combined with the workflow's `rm -rf "$pages_root"`, every PR's CI run wiped every other PR's visual review URL — clicking the "Visual review" check on an older PR returned 404 the moment any other PR's CI finished. Switch to `peaceiris/actions-gh-pages@v4` with `keep_files: true`, writing to a long-lived `gh-pages` branch. Each PR's content lands at `pr-<N>/<short_sha>/` (the URL the commit status posts) AND at `pr-<N>/latest/` (a stable per-PR link). Other PRs' directories on the branch are preserved. The job's `contents` permission moves from `read` to `write` so the action can push to gh-pages. `pages: write` / `id-token: write` are removed — they were only needed by the old deploy-pages flow. One-time repo setup (manual): Settings → Pages → Source must be set to "Deploy from a branch" with branch `gh-pages`. The action creates the branch on its first successful run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The auto-generated `shareableSnippets` map in
`parameters-calculations-citations.ts` dropped `updatedAt` from
`ShareableSnippet` and its entries earlier in this PR. The hand-edited
court-of-humanity referendum body still carried `updatedAt: "2026-05-03"`
plus a module comment claiming the canonical shape included it. No code
reads `.updatedAt` from any snippet, so drop the field from this module
and update the migration note to match the new shape
(`{ markdown, sourceFile, originalName }`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three issues, all valid: - **LiveCounter dropped `data-volatile`**, breaking `scripts/render-pages-to-markdown.ts` which uses that attribute to substitute deterministic placeholders into markdown previews (`pnpm copy:preview`). Restored — the component now emits BOTH attributes simultaneously, like death-counter and money-counter. - **LiveCounter kept ticking during visual review**, producing layout jitter as the span width grew from "1" → "10" → "100" even though the CSS mask hid the text. Added the same `__OPTIMITRON_VISUAL_REVIEW__` freeze death-counter / money-counter already use: when the flag is set, the component renders the placeholder text directly and skips the setInterval entirely. - **`pr-N/latest/` bare URL returned 404** because the directory only contained `latest.html`, not `index.html`. Workflow now copies `latest.html` to `index.html` in both the per-commit and per-PR latest directories so directory URLs resolve to the review page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three issues, all valid: - **LiveCounter dropped `data-volatile`**, breaking `scripts/render-pages-to-markdown.ts` which uses that attribute to substitute deterministic placeholders into markdown previews (`pnpm copy:preview`). Restored — the component now emits BOTH attributes simultaneously, like death-counter and money-counter. - **LiveCounter kept ticking during visual review**, producing layout jitter as the span width grew from "1" → "10" → "100" even though the CSS mask hid the text. Added the same `__OPTIMITRON_VISUAL_REVIEW__` freeze death-counter / money-counter already use: when the flag is set, the component renders the placeholder text directly and skips the setInterval entirely. - **`pr-N/latest/` bare URL returned 404** because the directory only contained `latest.html`, not `index.html`. Workflow now copies `latest.html` to `index.html` in both the per-commit and per-PR latest directories so directory URLs resolve to the review page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed Copilot's Valid (fixed): The hand-edited |
/politicians is a server-side redirect to /governments/US/politicians (same pattern as /impact). Auto-discovery in static-pages.ts was treating it as a public content page and asserting metadata, which a redirect never has. Adds ROUTES.politicians to CANDIDATE_REDIRECT_ONLY_PATHS so smoke tests it as a redirect (3xx + Location header) instead. Same shape as the /legal fix in ff1afad — both pages exist purely to preserve the legacy bookmark-friendly URL while the actual content lives at the canonical path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
a05097f to
8da9072
Compare
8da9072 to
c795f10
Compare
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
Code reviewCLAUDE.md violation: test transcribes implementation line-by-line File: This test re-asserts the same named constants used to define the Per CLAUDE.md: The |
voice-critic.md: rewrite goal-first instead of rule-first. The 6-section rubric made the agent pattern-match rules to diff verbatim — flagged <ParameterValue valueOverride="..."> as "defeats the component" when valueOverride is the intended API for attaching citation popovers with prose-friendly display text. New brief leads with the 5 questions the copy should answer for a stranger, lists banned phrases as hypotheses (verify against source before flagging), explicitly tells the agent to read ParameterValue.tsx before judging override usage. verify-ui-changes.mjs: scope voice gate to user-facing files only. It was scanning .claude/agents/voice-critic.md and flagging the banned-word list as banned-word usage. Skip .claude/, CLAUDE.md, AGENTS.md, TODO.md, .github/, .husky/ — those legitimately list banned terms as patterns to look for. Other gates (screenshot, copy-snapshot, test, reuse) already filter to packages/web/src/ paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary by CodeRabbit
New Features
Refactor
Tests
Documentation