# M4 · Product (T40–T44)

Exit gate: a complete cycle on testnet: the team publishes 5 candidates, people vote by email, the vote closes, the winner's operation starts automatically, and every dapp screen shows it live.

Design source: the Design canvas « Vivra · Landing & Dapp » (artboards Main, Vote, Candidate, Operating, Family). Ask the owner to share it with you. Copy layouts, colours, type and copy; do not redesign. Example data in the mockups must be replaced by API data.

---

## T40 · Email auth

**Goal** Passwordless email login with 6-digit code or magic link, no wallet.

**Depends on** T03

**Read first** `cycles-candidats-et-vote.md` (Vote par email), `annexe-api.md` (Connexion), `annexe-base-de-donnees.md` (`users`, `auth_codes`, `sessions`), `securite-et-observabilite.md`

**Build** `apps/api/src/routes/auth.ts`, `packages/core/src/email.ts` (normalize, disposable check), `apps/api/src/lib/{mailer.ts,crypto.ts,turnstile.ts}`, `packages/config/disposable-domains.txt`

**Steps**
1. Normalize email (trim, lowercase); reject disposable domains (list + setting for extras).
2. Store `email_hash` = HMAC-SHA256(EMAIL_HMAC_KEY) and `email_enc` = AES-256-GCM(EMAIL_ENC_KEY). Never log the email.
3. `POST /auth/email` (Turnstile required) → 6-digit code + magic token, 15 min TTL, both hashed; send via Resend/Postmark; always 204.
4. `POST /auth/verify` → 5 attempts max per code; creates session 30 days; cookie; returns `can_vote_at` = created_at + `min_account_age_hours`.
5. `GET /me`, `POST /auth/logout`.

**Done when**
- [ ] Tests: disposable domain → 422; 6th wrong code → code invalidated; expired code → 422
- [ ] DB inspection test: no plaintext email anywhere in tables
- [ ] Rate limits from annexe B enforced (tests)

---

## T41 · Cycles, candidates and vote

**Goal** The team composes and publishes a cycle; votes and supports work; the clock closes the vote and creates the operation.

**Depends on** T16, T22, T40

**Read first** `cycles-candidats-et-vote.md` (entire page), `annexe-api.md` (cycles, votes, supports, admin cycles), `parcours-1.md`, `parcours-2.md`

**Build** `apps/api/src/routes/{cycles,candidates,votes,supports}.ts`, `apps/api/src/routes/admin/cycles.ts`, `apps/workers/src/jobs/cycles.clock.ts`, `apps/admin/app/cycles/*`

**Steps**
1. Admin: create cycle (number, times, min account age; cycle 1 defaults to 0h), pick 5 validated tokens, edit dossier texts (tagline, thesis, reasons ×3, plan, risks 1–3), trigger Scanner pre-reports, publish (freezes recovery scores, sets `published_at`, tokens → `candidate`).
2. Public: `GET /cycles/current`, `GET /candidates/:id` (full dossier fields from annexe B), `GET /cycles/:id`.
3. `POST /votes` with all rules of annexe A "Règles à faire respecter"; change of vote allowed until close; WS `vote.tally` ≤ 1/s.
4. `POST/DELETE /supports`.
5. `cycles.clock`: opens votes at `vote_opens_at`; at `vote_closes_at` computes winner (tie → most supports → earliest position), sets J0, creates the operation (T22), losing candidates → `validated`.

**Done when**
- [ ] Vote rule tests: closed cycle, team account, too-new account, blocked account → refused with the right code
- [ ] Tie-break test
- [ ] E2E (API level): publish → 10 votes → close → operation exists with J0 = close time
- [ ] Frozen score on the dossier does not change when the live score changes (test)

---

## T42 · Dapp screens

**Goal** Vote, Dossiers, War room, Revived & ledger, wired to the API and WebSocket, matching the mockups.

**Depends on** T22, T33, T41

**Read first** `dapp-et-landing.md`, the Design canvas artboards Vote / Candidate / Operating / Family, `annexe-score-de-reprise.md` (Sur la dapp), `annexe-api.md` (WebSocket)

**Build** `apps/web/app/(dapp)/{vote,dossiers/[id],war-room/[id],ledger}/page.tsx`, `packages/ui/*` components (Header, PfpAvatar, SocialButtons, ScoreRing, PillarBars, Countdown, LogFeed, SliceGrid, LedgerTable…)

**Steps**
1. Build shared components first in `packages/ui` from the mockups (tokens: colours, radii, fonts).
2. Vote: launchpad cards (banner, pfp, socials, score ring, key figures, share bar), comparison matrix, email vote flow (email → code → voted / switch).
3. Dossier: tabs per candidate, banner + pfp + socials, market cap chart with peak and last-team-tweet markers, why we picked it, Scanner checks, how it died, takeover plan, risks, score breakdown with raw values, market, socials & docs, "if it wins".
4. War room: header, progress, chart with slice markers, KPIs, streaming public log with agent filter, agents' spend, shipped outputs, slices grid + recent tx, Pons CTO steps.
5. Revived & ledger: stats, weekly flywheel, family cards with verdict, filterable ledger, weekly burns, public wallets.
6. Every number links to its source (tx, contract or ledger row). Disclaimer banner on each page. Responsive at 390px. Reduced motion respected.

**Done when**
- [ ] Playwright: each screen renders with seeded data at 1440px and 390px, no console errors
- [ ] Voting flow e2e with a test inbox (Mailpit in docker) works end to end
- [ ] A new vote appears in shares in < 5s; a new log line appears in the war room without reload (e2e)
- [ ] Visual review against the mockups attached to the PR (screenshots side by side)

---

## T43 · Landing page

**Goal** The marketing landing, light theme with orange gradient, as in the Design canvas artboard « Landing ».

**Depends on** T00 (UI tokens from T42 if available)

**Read first** `dapp-et-landing.md`, Design canvas artboard Main (Landing)

**Build** `apps/web/app/(marketing)/page.tsx` + section components

**Steps**
1. Sections in order: hero (gradient, glass tray with the 4 step cards, rule numbers row), problem, agents (6 cards with output previews), flywheel (loop diagram, compounding points, the math card, coins paying in), 72-hour clock, recovery score, final CTA, footer.
2. Live values (voters, countdown, coins paying in) come from `/stats` and `/cycles/current`; anything not yet live is hidden, not faked.
3. SEO: title, description, OG image (generated with the brand template), sitemap, robots.

**Done when**
- [ ] Lighthouse ≥ 90 on performance, accessibility, SEO (CI)
- [ ] Playwright snapshot at 1440px and 390px
- [ ] No example number from the mockups is hard-coded (grep test for "3,812", "$GRIDL", "+58%")

---

## T44 · Vivra X bot

**Goal** Automatic posts of the Vivra account for every event of the spec.

**Depends on** T27, T31, T41

**Read first** `bot-x-et-integrations-sociales.md` (Posts automatiques, Règles d'automatisation)

**Build** `packages/templates/social/vivra/*.ts` (one template per event), event hooks emitting `social.publish` jobs, visual templates (rendered PNG)

**Steps**
1. Events: dossiers published (one per candidate), vote open, vote closed, operation milestones, each slice (after execution), verdict, weekly ledger, milestones.
2. Each template is a pure function `(data) → { text, media? }`, ≤ 280 chars, tested.
3. Respect rate rule and validation setting; slices are always posted after execution.

**Done when**
- [ ] Snapshot tests for every template with fixture data, all ≤ 280 chars
- [ ] Integration test: a slice execution emits exactly one post job, after the tx is confirmed
