teqo-backup/AGENTS.md

17 KiB
Raw Blame History

Payload CMS Development Rules

You are an expert Payload CMS developer. When working with Payload projects, follow these rules:

Teqo Project Context (read this first)

Teqo is the digital platform for deputado federal Jorge Solla (PT-BA): public site + editorial CMS + an internal /campanha campaign-management area, all in this one Next.js + Payload app. Full architecture/decision doc lives outside this repo at plano-arquitetura-campanha-2026.md (Cowork workspace) — check it for the "why" behind decisions below.

Product constraints (reopen only with new product evidence):

These are product/architecture constraints — not a ban on editing existing code. Engineering choices from past deliveries, plans, and project notebooks are history, not locks: prefer changing the module that already owns a concern over adding a twin beside it. What stays frozen for safety: shipped migration files (never edit retroactively), Consent/LGPD fail-closed behavior, and public URL contracts unless the delivery explicitly owns that change. See .agents/rules/engineering-standards.mdc ("Edit the owner, don't twin").

  • Single Next.js app, three route groups: (frontend) public site, (payload) admin, (campaign) internal campaign tool. No separate Rust service or frontend app for /campanha; it is modeled with Payload collections. The campaign roles are coordinator (label "Coordenador Geral"), advisor ("Assessor"), leader ("Liderança"), and candidate ("Candidato", unrestricted visibility), all on the separate auth collection campaignUser (see "Campaign auth" below). An advisor sees and manages ONLY the municipalities they administer; a leader is locked down to the supporter contact tool.

  • 2026-07-23 remodel (DEPLOYED to production 2026-07-23): "Município" is the operational unit (it replaced "Praça", which replaced "Núcleo Eleitoral"). Municípios are PREDEFINED (seeded, geography read-only): 435 rows — one per Bahia municipality (416), except Salvador (19 zone Municípios, ZE 1–19); Camaçari is a single whole municipality (ZE 170/171 aggregated); shared/leaky TSE zones are cut at the municipal boundary. Code identifier: municipality. Leaderships are one record per person (contact UNIQUE) linked to N municipalities and N organizations; vote pledges are per leadership×municipality with declaredVotes (staff-entered, leader-visible) vs estimatedVotes (three scenarios, staff-only, hidden from the leader). Campaign production data was reset by the remodel migrations (public-site data untouched). Master plan: docs/plans/remodelagem-municipios.md (supersedes remodelagem-pracas.md). Details: "Campaign Municípios model" below.

  • Production is self-hosted on the homeserver (jorgesolla1313.com.br, behind the Cloudflare tunnel; container/database teqo-1313/teqo_1313). There is no Vercel deploy anymore — the Vercel deploy job, vercel-promote.yml, the Vercel scripts/vercel.json and the VERCEL_* repo secrets were removed in OPS50 (2026-08-17). CI/PR/deploy AND the ISSUE TRACKER live on GitHub (OPS71 + OPS76, 2026-08-21) (github.com/fsolla/teqo/issues — labels, claims, pnpm issue/agent:* and the post-merge flips talk to the GitHub API; GitHub is the single host). The PR CI (ci-pr.yml, job único checks — OPS62) runs the same cascade as always (guards → lint → format → typecheck → knip → cycles → unit → int → build → e2e selected/afetado, skips via ci-scope.mjs); its check-run CI (PR) / checks is the required check of main — literal de match na proteção: checks (o GitHub casa pelo nome do check-run/job; a UI exibe CI (PR) / checks — pin ao vivo PR #742) — and the gate of the native auto-merge (agent-pr-ready-automerge.yml → scripts/github-pr-automerge.mjs, GraphQL enablePullRequestAutoMerge, rebase — armed with the AUTOMERGE_PAT real-user PAT, fail-closed without it; the built-in GITHUB*TOKEN would make the merge happen as github-actions[bot], whose events never create workflow runs — the post-merge closed flips would silently never run, live finding PR #746 (OPS71-FLIP) — the server only merges with the required check green). There is NO main verifier (ci.yml) anymore: publishing is a manual workflow_dispatch (.github/workflows/deploy.yml) with two jobs — verify (hosted ubuntu-latest, suíte full incl. e2e full) → deploy (needs: [verify], runs-on: [self-hosted, homeserver]): the GitHub self-hosted runner ON the homeserver executes scripts/deploy-homeserver.sh locally (no SSH; outbound-only runner, works behind the tunnel; the hosted job never touches the homeserver). The remote script is idempotent ("already deployed" → skip), builds the migrator image first, applies pending migrations through the compose maintenance service teqo-1313-migrate before the runner build (OPS66: the static build reads the new schema — build-after-migrate, never the reverse), builds the standalone image at the pushed SHA (BuildKit secrets from ~/stack/teqo-1313.env), pushes it to the local registry (localhost:5000), and recreates the container; the repo comes from TEQO_REPO_URL (default https://github.com/fsolla/teqo.git, public). pnpm push pushes to origin — the GitHub remote (one-time local cutover: git remote set-url origin git@github.com:fsolla/teqo.git; the Forgejo stays as a frozen secondary repo at the cutover point, its git repo and issue tracker inactive — OPS76 migrated the tracker to GitHub). The Forgejo-era files (.forgejo/workflows/, forgejo-pr-automerge.mjs, forgejo-dispatch.mjs, ci-classify-production.mjs) were removed in the OPS71 Fase 2 (2026-08-19); rollback to the Forgejo CI flow is a revert of that removal — the frozen Forgejo main still holds the full pre-cutover tree. Runbook (rollback, falhas conhecidas, passos do cutover): docs/ops/teqo-1313-deploy.md. The agent-pool supervisor workflow was killed in OPS65 (the */10tick cost 144 fullpnpm install/day on the workstation; agent-pool.yml is gone, pool scripts/skills stay dormant). The production database lives on the homeserver Postgres (teqo\*1313, migrated from Neon in OPS51, 2026-08-17) — the full production content (contacts, signatures, subscriptions, campaign) was restored there via pg_dump/pg_restore(dump in/srv/hdd/backups/teqo-neon-pre-migracao/); Neon remains alive and untouched as the documented rollback (re-point DATABASE_URLin~/stack/teqo-1313.env). Media moved to the homeserver Garage S3 in OPS52 (2026-08-18): the mediacollection uploads via@payloadcms/storage-s3when theS3\*\*envs are set, and Payload proxies the files through/api/media/file/...(the bucket stays private). Production env lives in~/stack/teqo-1313.env(bucketteqo-media, endpoint http://host.docker.internal:3900 — o container alcança o Garage no host via extra_hosts: host-gateway + regras ufw allow from 10.0.11.0/24 to any port 3900 (INPUT e route/FWD); a workstation/tailnet usa http://100.119.220.31:3900, key teqo-key); a partial S3\__config aborts the boot, and noS3\_\*means local disk (dev/test). The 40 pre-existing media rows are RECOVERED (OPS52-media, 2026-08-18): 38/40 objects restaurados no bucket a partir do WordPress (38 capas servindo 200 em/api/media/file/...; exceções sem fonte: fim-escala-6x1.jpgejorgesolla.jpg) — re-executar via pnpm media:recover(restaura objetos sem tocar no DB; runbook emdocs/plans/ops52-media-recuperar-arquivos-impl.md; desde OPS52-media-guard o reconcile exige MEDIA_RECOVER_CONFIRM=1— guard de intenção explícita para escrita no bucket,--dry-run/--verify seguem sem flag); the URLs themselves are the same contract the proxy now serves. Never point dev/test databases at the homeserver prod DB either — the local guards (guard-dev-db, assertTestDatabase) still enforce localhost/\_test-suffixed targets.

  • Donations are NOT handled in this app — they run through QueroApoiar (apoiar.me/jorgesolla, TSE-homologated). The site only needs a "Doar" link/CTA out to that URL.

  • Embasamento de produto/domínio: docs/research/ guarda o compêndio de literatura (campanha de DF na Bahia, geografia do voto, cartografia eleitoral) e o relatório de discovery aprovado (2026-07-21) com o playbook dado→decisão. Para métricas, análises territoriais, mapa e priorização em /campanha, consulte-o antes de inventar — o kernel: a disputa de DF é conta de quociente fragmentada; leitura útil é relativa e local (captura, LQ, quantis, cobertura de meta por pledges), nunca % estadual absoluto.

Real conventions already established in this codebase (follow these patterns; extend or edit the owner when the concern already exists — do not invent a parallel path):

  • Contact is the normalized "person" record (name, email, phone, state/city via src/lib/cities.ts, CEP). Don't create a parallel "person" collection for new features (e.g. líderanças/apoiadores) — add a join collection that relates to Contact, the same way Signature and Subscription do.
  • Every action that writes to more than one collection (see src/app/(frontend)/actions/*.ts) wraps the writes in a Payload transaction (payload.db.beginTransaction/commitTransaction/rollbackTransaction) and passes req: { transactionID } to every payload.create/payload.update call. Follow this pattern for any new multi-collection write.
  • LGPD consent is tracked via a Consent collection (richText, versioned, optional stable key) referenced by relationship from Signature/Subscription/campaign leadership. New campaign opt-in flows resolve Consent by stable key (e.g. lideranca-autopreenchimento) and fail closed when missing — don't invent a parallel consent mechanism or hardcode document IDs for new work.
  • Admin UI is organized via admin.group (e.g. 'Abaixo-assinados', 'Contatos', 'Configurações', 'Paginas', 'Campanha', 'Dados Eleitorais') — group new collections consistently instead of leaving them ungrouped.
  • i18n: Payload admin defaults to pt (not pt-BR — see payload.config.ts, payload/i18n/pt).
  • Language & naming conventions: ALL code identifiers are in English — variable, function, parameter, type/interface names, local constants, and Next.js dynamic route segment folder names used as param keys (e.g. [type]/[category]/[slug], not [tipo]/[categoria]). Portuguese is allowed ONLY in: user-visible string literals (JSX text, button/labels), image alt text, SEO/metadata values (title/description/keywords), Payload admin config text (labels/singular/plural/admin.description/field label), and intentional URL slug/enum VALUES kept in Portuguese for SEO (e.g. the post.type enum values noticia|campanha|artigo|evento, tag/category slugs like saude, eleitoral). Never translate those data/URL values or admin labels. Renaming a route segment folder changes only the param KEY, never the public URL (segment values come from the data).

Database & local development (safety-critical — production is the homeserver Postgres teqo_1313 with real citizens' PII):

  • Local dev and tests must NEVER touch production. Local dev runs against a local Docker Postgres (pnpm db:start); pnpm dev refuses a non-local DATABASE_URL (guard: scripts/guard-dev-db.mjs) unless ALLOW_REMOTE_DB=true. Tests load .env.test (teqo_test) and refuse any database whose name doesn't end in _test (tests/helpers/assertTestDatabase.ts). Never repoint .env/.env.local/.env.test DATABASE_URL at the homeserver prod DB (teqo_1313). Full workflow lives in the local-database skill (.agents/skills/local-database).
  • pnpm dev honra o PORT do .env.local (OPS40): next dev resolve a porta ANTES de @next/env carregar os env-files (commander .env('PORT') no bin/next), então o PORT=3100+slot do provisionador de worktree era ignorado e todo dev server subia em 3000. pnpm dev hoje passa por scripts/dev.mjs: roda o mesmo preflight do guard-dev-db, carrega .env.local → .env (override:false — env real do processo vence, como o PORT que o webServer do Playwright injeta) e entrega a porta ao next dev como flag CLI -p (resolveDevPort/nextDevArgs em scripts/lib/cli.mjs, unit-testados); sem PORT em lugar nenhum, nenhum -p é passado e o default 3000 (com allowRetry) do Next segue intacto no main repo.
  • Schema changes go through committed Payload migrations — push: false everywhere, never flip it on against a remote DB. Edit configs → pnpm migrate:create <name> → commit src/migrations/* (both .ts and .json, plus index.ts) → pnpm migrate locally. pnpm build runs payload migrate, so every production deploy applies pending migrations to prod. Prod is baselined at migration 20260715_163458_initial — never regenerate or replace the initial migration. Full workflow (incl. hand-written data/reconciliation migrations) lives in the payload-migrations skill (.agents/skills/payload-migrations).
  • Consent.text was reconciled from varchar to jsonb via 20260715_163500_consent_text_to_jsonb (matching the richText field). Any future field-type change is a migration, not a manual DB edit.

Histórico de entregas (movido)

"Known Gaps" e os blocos "Recently resolved" saíram deste arquivo em 2026-07-30: o registro commitado vive em docs/changelog/ (uma entrada por arquivo, imune a conflito). O agregado legível docs/CHANGELOG-AGENTS.md é gitignored e gerado sob demanda (OPS85): pnpm changelog:build grava, pnpm changelog:read imprime no stdout — com seed do snapshot congelado docs/CHANGELOG-AGENTS-HISTORY.md. Novas entregas registram UMA entrada curta em docs/changelog/<data>-<id>.md — nunca editam docs/CHANGELOG-AGENTS.md nem o HISTORY na mão. A referência genérica de Payload (config/collections/fields/hooks/queries/components) foi para docs/PAYLOAD-REFERENCE.md — consulta sob demanda, não always-on. Operação do paradigma de agentes paralelos (claim→PR→main→deploy gated): docs/AGENT-OPS.md.

Core Principles

  1. TypeScript-First: Always use TypeScript with proper types from Payload
  2. Security-Critical: Follow all security patterns, especially access control
  3. Type Generation: Run generate:types script after schema changes
  4. Transaction Safety: Always pass req to nested operations in hooks
  5. Access Control: Understand Local API bypasses access control by default
  6. Access Control: Ensure roles exist when modifiyng collection or globals with access controls

Code Validation

  • To validate typescript correctness after modifying code run tsc --noEmit
  • Generate import maps after creating or modifying components.
  • Any change to a collection/global/field schema requires a migration: pnpm migrate:create <name>, then pnpm migrate locally. Never rely on push (it is false). See the payload-migrations skill.

Project Structure

Actual layout (campaign access control lives in src/utilities/access/* domain modules with src/utilities/campaignAccess.ts as the re-export surface; hooks are still written inline inside each collection/global file):

src/
├── app/
│   ├── (frontend)/          # Frontend routes + server actions (src/app/(frontend)/actions/*.ts)
│   ├── (payload)/           # Payload admin routes
│   └── (campaign)/          # Internal campaign-management area (auth barrier, see "Campaign auth")
├── collections/             # Users, CampaignUser, CampaignWebAuthnCredential, CampaignInvite, Municipality, Leadership, VotePledge, MunicipalityUpdate, Organization, StateDeputy, CampaignDemand, AllocationDecision, Supporter, SupporterImportBatch, Activity, ElectionTally, ElectionCandidateVote, ElectionCandidate, Media, Petition, Signature, Consent, Contact, Subscription, Post, Tag
├── globals/                 # Global configs (SiteSettings, HomePage, Metadata, PrivacyPolicy, CampaignGoals)
├── components/              # campaign/<domínio>/ (municipality, activity, supporter, …, shared, shell — Pass 2 W2), ui from shadcn
├── lib/                     # cities.ts, bahiaTerritories.ts, bahiaTseZones.ts, bahiaMunicipalityCodes.ts, bahiaGeometries.ts, geometries/*.topo.json, municipalityCatalog.ts, electionAggregates/ (committed TSE artifact) + bahiaElectionAggregates.ts, formData.ts, zod schemas (lib/schemas/*)
├── utilities/               # campaign*, access/* (per-domain RBAC; campaignAccess.ts re-exports), municipality*, posts, documents, globals, locks, etc.
└── payload.config.ts        # Main config

Per-worktree environments

pnpm worktree next provisions isolated envs per worktree: dev port 3100+slot, databases teqo_wt<slot> / teqo_wt<slot>_test on the shared container with full migration chain + minimal seed, .env.local + .env.test.local (gitignored), copies of PAYLOAD_SECRET/S3_*/VAPID from main repo. The skill local-database covers the full workflow.