Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 44 additions & 27 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ jobs:
timeout-minutes: 30
permissions:
actions: read
contents: read
# peaceiris/actions-gh-pages needs write to push the visual review to
# the long-lived `gh-pages` branch.
contents: write
deployments: read
id-token: write
issues: write
pages: write
pull-requests: write
statuses: write
env:
Expand Down Expand Up @@ -314,22 +314,39 @@ jobs:
packages/web/test-results
if-no-files-found: ignore

- name: Prepare visual review Pages site
# Publish the per-PR visual review to the long-lived `gh-pages` branch
# so reviews from different PRs don't clobber each other. The previous
# setup used `actions/deploy-pages`, which atomically REPLACES the
# entire Pages site each deploy — so whenever any PR's CI finished,
# every other PR's review URL went 404 until that PR's CI ran again.
#
# `peaceiris/actions-gh-pages` with `keep_files: true` writes only the
# current PR's directory and preserves everything else on `gh-pages`.
#
# One-time repo setup: Settings → Pages → Source must be set to
# "Deploy from a branch" with branch `gh-pages` (root). The action
# creates the branch on its first push.
- name: Prepare per-PR visual review directory
if: always() && github.event_name == 'pull_request'
id: prepare_pages
shell: bash
run: |
pages_root="packages/web/output/playwright/pages"
pr_dir="$pages_root/pr-${{ github.event.pull_request.number }}"
head_sha="${{ github.event.pull_request.head.sha }}"
short_sha="${head_sha:0:12}"
commit_dir="$pr_dir/$short_sha"
rm -rf "$pages_root"
mkdir -p "$pr_dir" "$commit_dir"

cp -R packages/web/output/playwright/review/. "$pr_dir/"
pr_root="packages/web/output/playwright/pages-per-pr/pr-${{ github.event.pull_request.number }}"
commit_dir="$pr_root/$short_sha"
latest_dir="$pr_root/latest"
rm -rf "$pr_root"
mkdir -p "$commit_dir" "$latest_dir"
cp -R packages/web/output/playwright/review/. "$commit_dir/"

cat > "$pages_root/index.html" <<'HTML'
cp -R packages/web/output/playwright/review/. "$latest_dir/"
# Seed `.nojekyll` and a root `index.html` at the publish root so the
# gh-pages site doesn't try to render through Jekyll and so the
# bare https://<user>.github.io/<repo>/ URL renders something
# meaningful. `keep_files: true` preserves these across deploys.
publish_root="packages/web/output/playwright/pages-per-pr"
touch "$publish_root/.nojekyll"
cat > "$publish_root/index.html" <<'HTML'
<!doctype html>
<html lang="en">
<head>
Expand All @@ -339,26 +356,26 @@ jobs:
</head>
<body>
<h1>Optimitron Visual Reviews</h1>
<p>Open the pull request comment for the latest visual review link.</p>
<p>Open the "Visual review" check on a pull request for its review page.</p>
</body>
</html>
HTML
touch "$pages_root/.nojekyll"

- name: Configure GitHub Pages
if: always() && github.event_name == 'pull_request'
uses: actions/configure-pages@v5
echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
echo "publish_dir=packages/web/output/playwright/pages-per-pr" >> "$GITHUB_OUTPUT"

- name: Upload GitHub Pages artifact
if: always() && github.event_name == 'pull_request'
uses: actions/upload-pages-artifact@v3
with:
path: packages/web/output/playwright/pages

- name: Deploy visual review to GitHub Pages
- name: Publish visual review to gh-pages
if: always() && github.event_name == 'pull_request'
id: visual_review_pages
uses: actions/deploy-pages@v4
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ${{ steps.prepare_pages.outputs.publish_dir }}
publish_branch: gh-pages
keep_files: true
enable_jekyll: false
commit_message: "Visual review for pr-${{ github.event.pull_request.number }}@${{ steps.prepare_pages.outputs.short_sha }}"
user_name: github-actions[bot]
user_email: github-actions[bot]@users.noreply.github.com

- name: Post Visual review commit status
if: always() && github.event_name == 'pull_request'
Expand Down
61 changes: 40 additions & 21 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,13 @@ supporting detail or parked work; they should not override this sequence.
benchmark children are retired.
4. [x] **Wire production deploy to run managed-data sync.** This prevents future
canonical task/title/trigger changes from requiring one-off data migrations.
5. [ ] **Then update the UI presentation.** `/tasks/optimize-earth`, dashboard, and
visual-review routes should show the simplified tree, while `warondisease.org`
5. [~] **Then update the UI presentation.** Dashboard is now a focused share
card + collapsed disclosures (PR #71); /tasks/optimize-earth and the
visual-review routes still want a simplified tree view. War on Disease
still pushes the 1% Treaty vote first.
6. [ ] **Only add `allowsUserSubtasks` before exposing public subtask creation UI.**
Seeded/admin-managed task trees can ship before this. Public UGC needs the
permission column/guard so arbitrary users cannot clutter canonical parents.
6. [] **`allowsUserSubtasks` schema column — parked.** Existing schema is
sufficient to add subtasks when needed. Revisit only when public subtask
creation UI is on the immediate roadmap.
7. [x] **Fold task triggers into managed data after the task-tree sync is proven.**
Trigger definitions are the same kind of semi-permanent app data and no
longer use a separate one-off production seed path.
Expand All @@ -213,9 +214,11 @@ cross-check so they do not disappear into chat history.

**Dashboard and task UX**

- Replace the logged-in dashboard with a short action checklist:
sign the 1% Treaty, render verdict, register plaintiff, summon jurors,
pressure/manage presidents. Link each row to the actual page.
- [x] Replace the logged-in dashboard with a focused share surface: plaintiff
status, share message + copy button, collapsed disclosures for register-a-plaintiff /
remind-overdue-presidents / endorse / assigned-tasks. Shipped PR #71 — the
composer, leaderboard, plaintiff form, and full treaty text are no longer
embedded; each lives only on its dedicated page.
- Remove the generic task-detail metadata block where it duplicates header
information. Keep title, assignee, due date, primary action, markdown body,
comments, complete/reassign/admin controls.
Expand All @@ -228,8 +231,29 @@ cross-check so they do not disappear into chat history.
- Add E2E coverage that a signed-in demo user can open an assigned/private task
from "Your Tasks" without hitting 404.

**Post-vote email (decided 2026-05-11)**

- Build a single triggered email (not a drip) fired the moment `ReferendumVote.status == COUNTED`.
- Body is the "I love you and don't want you to suffer and die of horrible
diseases" share message verbatim, with the user's referral URL inline.
- CTA above the body: "Hit forward, paste two email addresses, send. That's
the whole job." The email *is* the forward-friendly recruitment vehicle, not
a screen the user has to copy from.
- Reuses existing Resend pipeline + reply-handling + unsubscribe-on-reply rails.
- No subsequent drips. Reminder spam is explicitly banned (see PR #66
"Disable generic overdue email reminders").

**Visual review and preview workflow**

- [x] Visual review surfaces a `Visual review` commit status pointing to the
SHA-pinned gallery in the merge box (PR #71), replacing the 6-link bot
comment that buried the actual gallery under filler.
- Add email-template screenshots to the visual review HTML. Render each email
template (`buildMagicLinkHtml`, task-assignment, task-comment-notification,
inbound-monitor-forward, the future post-vote forward email) with a
representative token set, screenshot, and emit alongside the page
screenshots in `latest.html`. Reviewers currently can't see email copy
without setting up Resend locally + emailing themselves a test.
- Add the Central Time generation timestamp to visual review HTML.
- Use commit-hash or otherwise cache-busted GitHub Pages paths for generated
visual reviews so a new PR comment cannot show an old cached `latest.html`.
Expand Down Expand Up @@ -449,19 +473,14 @@ The IAM smoke-test email I wrote first would have failed this lint
on the word-count rule alone — exactly the regression class the
human flagged. Schema-zero. Cheap.

### P1 — Plaintiff damages surface on `/plaintiffs/page.tsx`

`/plaintiffs/page.tsx` imports `WAR_DEATHS_SINCE_1900` and
military-spending parameters but not
`CORPORATE_DAMAGES_FORWARD_SETTLEMENT_VALUE_PER_CAPITA` or the
cohort constant. So a visitor sees the gallery without learning
what each registered plaintiff is owed (~$10.6M NPV / $25.2M
lifetime cohort). The case page surfaces this; the registration
page should too. ~30 lines of JSX added under the existing
parameter imports — schema-zero. Highest per-line conversion lift
on the to-do list right now; a visitor who lands on `/plaintiffs`
from the case-page CTA without first reading the case currently
has no damages number to anchor on.
### P1 — Plaintiff damages surface on `/plaintiffs/page.tsx` ✅ done

Shipped: the page renders a "Demanded recovery per registered plaintiff"
section above the conversion form with the $10.6M NPV (per
`CORPORATE_DAMAGES_FORWARD_SETTLEMENT_VALUE_PER_CAPITA`) and the
$25.2M lifetime cohort (per `LOST_PROSPERITY_LIFETIME_DAMAGES_PER_CAPITA`)
side-by-side, then an explanation of the family-share frame and the
4B-vote enforcement gate.

### P1 — Centralize communication templates (audit findings 2026-05-08)

Expand Down
54 changes: 0 additions & 54 deletions packages/db/src/task-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,57 +135,3 @@ export const ACCOUNTABILITY_TASK_KEY_PREFIX = "accountability";
export function buildAccountabilityTaskKey(countryCode: string, activitySlug: string) {
return `${ACCOUNTABILITY_TASK_KEY_PREFIX}:${countryCode.toLowerCase()}:${activitySlug}`;
}

// Optimize-Earth system tasks (autonomous agent's per-action subtasks)

export const OPTIMIZE_EARTH_TASK_KEY_PREFIX = "system:optimize-earth";

// Static optimize-earth subkeys (one row each in the seed/queue).
export const OPTIMIZE_EARTH_WEAPONIZE_OVERDUE_TASK_LIST_KEY = `${OPTIMIZE_EARTH_TASK_KEY_PREFIX}:weaponize-overdue-task-list`;
export const OPTIMIZE_EARTH_CROSSLINK_PAGES_KEY = `${OPTIMIZE_EARTH_TASK_KEY_PREFIX}:crosslink-task-government-politician-pages`;
export const OPTIMIZE_EARTH_CONVERT_PITCH_PAGES_KEY = `${OPTIMIZE_EARTH_TASK_KEY_PREFIX}:convert-pitch-pages-into-task-traffic`;
export const OPTIMIZE_EARTH_DISCOVER_OFFICE_CHANNELS_KEY = `${OPTIMIZE_EARTH_TASK_KEY_PREFIX}:discover-missing-signer-office-channels`;
export const OPTIMIZE_EARTH_DISCOVER_PRESS_KEY = `${OPTIMIZE_EARTH_TASK_KEY_PREFIX}:discover-country-journalist-and-coalition-targets`;
export const OPTIMIZE_EARTH_GROUND_GENERATOR_KEY = `${OPTIMIZE_EARTH_TASK_KEY_PREFIX}:ground-task-generation-in-existing-pages-and-manual`;
export const OPTIMIZE_EARTH_GENERATE_GROWTH_TASKS_KEY = `${OPTIMIZE_EARTH_TASK_KEY_PREFIX}:generate-growth-conversion-tasks`;
export const OPTIMIZE_EARTH_GENERATE_CONTACT_DISCOVERY_KEY = `${OPTIMIZE_EARTH_TASK_KEY_PREFIX}:generate-contact-discovery-tasks`;
export const OPTIMIZE_EARTH_GENERATE_SYSTEM_IMPROVEMENT_KEY = `${OPTIMIZE_EARTH_TASK_KEY_PREFIX}:generate-system-improvement-tasks`;

// Action-follow-through dynamic subkeys (one tree per action).
export const OPTIMIZE_EARTH_ACTION_FOLLOW_THROUGH_PREFIX = `${OPTIMIZE_EARTH_TASK_KEY_PREFIX}:action-follow-through`;
export const OPTIMIZE_EARTH_PUBLISH_BUDGET_BRIEF_PREFIX = `${OPTIMIZE_EARTH_TASK_KEY_PREFIX}:publish-budget-brief`;
export const OPTIMIZE_EARTH_ROUTE_PROOF_PAGES_PREFIX = `${OPTIMIZE_EARTH_TASK_KEY_PREFIX}:route-proof-pages-into-funding`;
export const OPTIMIZE_EARTH_SOURCE_COUNTERPARTIES_PREFIX = `${OPTIMIZE_EARTH_TASK_KEY_PREFIX}:source-counterparties-and-price-ceiling`;
export const OPTIMIZE_EARTH_PREPARE_APPROVAL_PACKET_PREFIX = `${OPTIMIZE_EARTH_TASK_KEY_PREFIX}:prepare-approval-packet`;

/**
* Prefixes whose presence on a taskKey means the action-follow-through tree
* has already been spawned for that task — used to avoid recursive spawning.
*/
export const OPTIMIZE_EARTH_ACTION_FOLLOW_THROUGH_KEY_PREFIXES = [
`${OPTIMIZE_EARTH_ACTION_FOLLOW_THROUGH_PREFIX}:`,
`${OPTIMIZE_EARTH_PUBLISH_BUDGET_BRIEF_PREFIX}:`,
`${OPTIMIZE_EARTH_ROUTE_PROOF_PAGES_PREFIX}:`,
`${OPTIMIZE_EARTH_SOURCE_COUNTERPARTIES_PREFIX}:`,
`${OPTIMIZE_EARTH_PREPARE_APPROVAL_PACKET_PREFIX}:`,
] as const;

export function buildOptimizeEarthActionFollowThroughKey(taskSlug: string) {
return `${OPTIMIZE_EARTH_ACTION_FOLLOW_THROUGH_PREFIX}:${taskSlug}`;
}

export function buildOptimizeEarthPublishBudgetBriefKey(taskSlug: string) {
return `${OPTIMIZE_EARTH_PUBLISH_BUDGET_BRIEF_PREFIX}:${taskSlug}`;
}

export function buildOptimizeEarthRouteProofPagesKey(taskSlug: string) {
return `${OPTIMIZE_EARTH_ROUTE_PROOF_PAGES_PREFIX}:${taskSlug}`;
}

export function buildOptimizeEarthSourceCounterpartiesKey(taskSlug: string) {
return `${OPTIMIZE_EARTH_SOURCE_COUNTERPARTIES_PREFIX}:${taskSlug}`;
}

export function buildOptimizeEarthPrepareApprovalPacketKey(taskSlug: string) {
return `${OPTIMIZE_EARTH_PREPARE_APPROVAL_PACKET_PREFIX}:${taskSlug}`;
}
18 changes: 17 additions & 1 deletion packages/web/src/components/tasks/live-counter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ interface LiveCounterProps {
/** Tick every 250ms — 4 updates/sec, smooth enough to feel live, cheap on CPU. */
const TICK_INTERVAL_MS = 250;

/**
* Visual-review placeholders. The e2e visual-regression spec swaps any node
* with `data-visual-mask="dynamic"` to render its `data-visual-placeholder`
* text instead of the live value, so screenshots stay byte-identical across
* runs. Without this, every CI run captured a different tick of the counter
* and produced false-positive diffs on every page that embeds a LiveCounter
* (notably /employees, /presidents, and signer rows).
*/
const VISUAL_REVIEW_INTEGER_PLACEHOLDER = "123,456";
const VISUAL_REVIEW_CURRENCY_PLACEHOLDER = "$123,456,789,012";

const intFormatter = new Intl.NumberFormat("en-US", {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
Expand Down Expand Up @@ -59,7 +70,12 @@ export function LiveCounter({
return (
<span
className={className}
data-volatile={mode === "currency" ? "money" : "count"}
data-visual-mask="dynamic"
data-visual-placeholder={
mode === "currency"
? VISUAL_REVIEW_CURRENCY_PLACEHOLDER
: VISUAL_REVIEW_INTEGER_PLACEHOLDER
}
Comment on lines +73 to +78

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain volatile marker on live counters

Add back a data-volatile attribute on this <span> (e.g., money/count) instead of replacing it entirely with visual-mask attrs. The copy-preview pipeline (packages/web/scripts/render-pages-to-markdown.ts, extractPage) only normalizes nodes selected by [data-volatile]; without that marker, live counter text is captured as wall-clock values, so page.logged-*.md outputs churn between runs and obscure real copy diffs.

Useful? React with 👍 / 👎.

suppressHydrationWarning
>
{displayValue ?? "…"}
Expand Down
53 changes: 0 additions & 53 deletions packages/web/src/lib/optimize-earth-grounding.server.test.ts

This file was deleted.

48 changes: 0 additions & 48 deletions packages/web/src/lib/optimize-earth-grounding.server.ts

This file was deleted.

Loading
Loading