diff --git a/docs/plans/escala-dry-pos-c2.md b/docs/plans/escala-dry-pos-c2.md
index 0f228d71..ac42ef82 100644
--- a/docs/plans/escala-dry-pos-c2.md
+++ b/docs/plans/escala-dry-pos-c2.md
@@ -155,7 +155,7 @@ Todas as cinco fases foram implementadas e validadas localmente (`tsc --noEmit`,
- **Bulk insert deve rodar na txn Payload existente, não numa txn drizzle nova.** O seed TSE (`electionResultsImport.ts`) abre `dbAdapter.drizzle.transaction(...)` própria; para C4 isso quebraria a atomicidade com os locks advisory. Usado `getPostgresTransactionDatabase(payload, req)` para pegar a sessão drizzle vinculada à txn Payload.
- **`skipContactPhoneInvariant` é fail-closed por txn, não por introspecção de lock.** Não há maneira barata de perguntar ao PG "seguro o lock X?"; o proxy usado é `req.transactionID` ativo (o lock `pg_advisory_xact_lock` é xact-level e só existe dentro de uma txn). Sem txn + flag setado → throw 500.
- **`supporter.beforeChange` (coexistência com liderança) é no-op para import sem núcleo.** O hook só dispara quando `contactID` e `nucleusID` estão presentes; import cria supporters sem núcleo, então o bypass via drizzle insert é seguro. `createdBy` é setado explicitamente no row bulk.
-- **Escopo do mapper de form errors:** `ActionPlanPagination` e os forms de planos também consomem `campaignListUrl`/`CampaignListPagination`, mas o mapper de form actions de planos ficou para C7 (escopo de copy/mensagens diferente).
+- **Escopo do mapper de form errors:** `ActionPlanPagination` e os forms de planos também consomem `campaignListUrl`/`CampaignListPagination`. O mapper de form actions de planos (`planos/formActions.ts`, `planos/[slug]/updateFormActions.ts`) ficou de fora do escopo original do C7 (copy/mensagens diferentes) mas foi consolidado pelo C8 (`escala-dry-pos-c6.md`), junto com os demais `formActions` restantes — C7 Fase 3 manteve apenas os helpers de FormData/URL de território do `actionPlan`.
## Simplify (2026-07-19)
diff --git a/docs/plans/escala-dry-pos-c6.md b/docs/plans/escala-dry-pos-c6.md
index f03b1a51..0b6695f7 100644
--- a/docs/plans/escala-dry-pos-c6.md
+++ b/docs/plans/escala-dry-pos-c6.md
@@ -1,6 +1,6 @@
# Escala e DRY pós-C6 (apoiadores / import / listas)
-Status: rascunho
+Status: implementado (pendente merge em `main`)
Atualizado em: 2026-07-19
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha C, item C8)
Responsável: —
@@ -118,3 +118,17 @@ flowchart TD
- `src/utilities/campaignAccess.ts` — `getAccessibleNucleusIds`
- `src/utilities/campaignFormActionError.ts` / `campaignFormFields.ts` — helpers adotados por 2 forms
- AGENTS.md — `overrideAccess: false`, transações, locks advisory xact-level, naming
+
+## Simplify (2026-07-19)
+
+Duas passagens `/simplify` sobre o diff do C8 (pré- e pós-rebase em `main` com C7) aplicaram limpezas pontuais sem mudar comportamento: `safeMessages` opcional em `mapCampaignFormActionError`, `drizzleResultRows` reutilizado em `campaignInviteRepository`, chunk de `IN` nas leituras drizzle do bulk import, `select: { id: true }` via `DynamicFind` em `getCoordinatorNucleusIds`, prefetch de núcleos do coordenador para o overview, `errorProps` no `CampaignInviteForm`/`NucleusUpdateForm`, e hoisting de erros de campo. Validado com `tsc --noEmit`, `pnpm lint`, `pnpm test:int`, `pnpm build` e scan Aikido.
+
+Os débitos que os revisores marcaram como importantes e maiores que cleanup foram registrados como item **C9** — [escala-dry-pos-c8.md](escala-dry-pos-c8.md):
+
+1. **Filtros duplicados** — `buildSupporterListWhere` (Payload) vs `buildAggregateSql` (SQL cru); risco de drift entre KPIs e lista.
+2. **`contactSearchQuery` não wireado no aggregate** — overview ainda dispara `ILIKE` para `q` de 1 caractere apesar do `pg_trgm` e do guard de C7.
+3. **Lookup de núcleos do coordenador repetido** — até 3× por navegação em `/campanha/apoiadores` (list access + options + overview prefetch).
+4. **`apoiadores/[id]/formActions.ts` fora do mapper** — contrato `message` vs `fieldErrors` exige desenho antes de migrar.
+5. **Componentes de form com `fieldError` local** — `NucleusForm`, `ActionPlanForm`, `NucleusTerritoryFields`, `VoteEstimateDialog`, etc.
+
+**Explicitamente fora (tradeoffs aceitos no C8):** waterfall lista→overview (precisa de `totalDocs`); unificar lista+aggregate numa query; widen de `PostgresTransactionDatabase`.
diff --git a/docs/plans/escala-dry-pos-c8.md b/docs/plans/escala-dry-pos-c8.md
new file mode 100644
index 00000000..c0eab80d
--- /dev/null
+++ b/docs/plans/escala-dry-pos-c8.md
@@ -0,0 +1,108 @@
+# Escala e DRY pós-C8 (apoiadores / filtros / forms)
+
+Status: registrado no roadmap (fases pendentes)
+Atualizado em: 2026-07-19
+Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha C, item C9)
+Responsável: —
+
+## Contexto
+
+O C8 ([escala-dry-pos-c6.md](escala-dry-pos-c6.md)) entregou a segunda camada de escala/DRY do cadastro nominal: locks advisory em um round-trip, bulk import com `.returning()` e leituras drizzle na txn, overview sem `COUNT(*)` redundante, migration `pg_trgm` em `contact`, helper `drizzleBulk.ts`, variante `getCoordinatorNucleusIds`, e migração da maioria dos `formActions` para `mapCampaignFormActionError` + `campaignFormFields`. Duas passagens `/simplify` (pré- e pós-rebase em `main` com C7) aplicaram limpezas adicionais (`select: { id: true }` via `DynamicFind`, prefetch de núcleos do coordenador para o overview, chunk de `IN` no import, `drizzleResultRows` no `campaignInviteRepository`, `errorProps` no `CampaignInviteForm`, `safeMessages` opcional no mapper).
+
+Os revisores (quality / reuse / performance) marcaram como **importantes e maiores que cleanup** os follow-ups abaixo. Sem registro, a lista de apoiadores pode divergir entre Payload e SQL agregado, o coordenador paga 2–3 lookups idênticos por navegação, e o DRY de forms fica incompleto na borda da vertical.
+
+1. **Filtros de apoiadores duplicados.** `buildSupporterListWhere` (`src/utilities/supporterUi.ts`, Payload `Where`) e `buildAggregateSql` (`src/utilities/supporterListOverviewAggregate.ts`, SQL cru) reimplementam a mesma semântica (telefone normalizado, `q`, `voteIntention`, `city`, `nucleus`). O C8 derivou `total` da lista, mas os dois caminhos ainda podem divergir em qualquer mudança futura.
+2. **`contactSearchQuery` não wireado no aggregate.** C7 entregou `isContactSearchQueryReady` / `CONTACT_SEARCH_MIN_LENGTH` em `src/lib/contactSearchQuery.ts` (mín. 2 chars) para comboboxes; o aggregate SQL do overview ainda aplica `ILIKE '%q%'` para `q` de 1 caractere — desperdiça o `pg_trgm` do C8.
+3. **Lookup de núcleos do coordenador repetido na página de apoiadores.** `getCoordinatorNucleusIds` roda no prefetch do overview, mas `loadSupporterListPageData` e `loadAccessibleNucleusOptions` disparam access separado (`getAccessibleNucleusIds` → mesma query) sem `req.context` compartilhado em RSC — até 3× por navegação para `coordenador`.
+4. **`apoiadores/[id]/formActions.ts` fora do mapper compartilhado.** C8 migrou ~8 `formActions`, mas o detalhe do apoiador mantém ladder inline com contrato de erro diferente (`message` em validação Zod, não `fieldErrors`) — drift e risco de regressão se migrado sem desenho.
+5. **Componentes de form com `fieldError` local.** `NucleusForm`, `ActionPlanForm`, `NucleusTerritoryFields`, `VoteEstimateDialog`, `ActionPlanUpdateForm`, etc. ainda usam `errorFor` ou `fieldErrors?.[0]` em vez de `campaignFormFields.ts` — fora do escopo mínimo do C8 Fase 4 (só `NucleusUpdateForm` + `CampaignInviteForm`).
+
+**Explicitamente fora (revisores pediram skip no simplify):** remover o waterfall lista→overview (tradeoff intencional para `totalDocs` correto); unificar lista+aggregate numa query só (só se latency reclamar); migrar `collectLockParams` do teste de locks; widen de `PostgresTransactionDatabase` (follow-up de plataforma, coberto parcialmente por `assertDrizzleColumns`).
+
+## Objetivos
+
+- Um único núcleo de intenção de filtro de apoiadores alimenta Payload (`Where`) e SQL agregado — sem drift entre KPIs e lista.
+- Busca `q` no overview (e, se ainda faltar, na lista Payload) respeita `contactSearchQuery` (mín. 2 chars ou dígitos suficientes), alinhado ao C7.
+- Página `/campanha/apoiadores` resolve escopo de núcleos do coordenador **uma vez** por render e repassa para list/overview/options.
+- Forms restantes da campanha usam `mapCampaignFormActionError` / `fieldError` / `errorProps` sem quebrar contratos de UI existentes.
+- Guardrails: sem novo `Consent`; sem migration salvo se unificar filtros exigir índice novo (improvável); `overrideAccess: false` com `user`; locks advisory xact-level inalterados.
+
+## Decisões travadas
+
+- **Um item C9, quatro fases ordenadas.** Mesmo racional do C6/C7/C8: um ID de roadmap, PRs por fase. Ordem: filtros (correção) → `contactSearchQuery` + loader da página → forms restantes (detalhe + componentes).
+- **Dependência dura de C8 (merge).** O código otimizado é o do C8; não reabre decisões de v1 do C6 (token HMAC, `supporterImportBatch`, `skipContactPhoneInvariant`).
+- **Dependência suave de C7** para `contactSearchQuery` — já em `main`; C9 só wireia nos paths de apoiadores que ainda ignoram o guard.
+- **Cortável se a base nominal permanecer pequena.** Fases 1–2 só rendem com volume real ou coordenadores ativos; Fases 3–4 (DRY de forms) são baratas e reduzem drift — manter se houver folga.
+- **`apoiadores/[id]/formActions`:** migrar só com `resolveBoundaryMessage` / branch pré-mapper que preserve `message` onde a UI espera texto global (não field error).
+- **i18n e naming** (AGENTS.md): identificadores em inglês (`buildSupporterListFilterConditions`, `loadSupportersPageData`), strings visíveis em pt-BR.
+
+## Questões em aberto
+
+- **Filtro unificado: AST neutro vs helper de termos de busca?** **Recomendação:** extrair pelo menos `buildSupporterSearchTerms(q)` compartilhado (telefone, dígitos, ILIKE) na Fase 1; AST completo Payload↔SQL só se a Fase 1 ainda duplicar branches de `voteIntention`/`nucleus`.
+- **Memo de núcleos: `React.cache(getCoordinatorNucleusIds)` vs `loadSupportersPageData`?** **Recomendação:** loader único `loadSupportersPageData` que retorna `{ list, overviewInputs, nucleusOptions, coordinatorNucleusIds }` — mais explícito que cache implícito e testável em int.
+- **Migrar componentes de form fora do diff do C8?** **Recomendação:** sim na Fase 4 — são o mesmo débito DRY da vertical; escopo mecânico (`fieldError` / `errorProps`), sem mudar copy.
+
+## Abordagem proposta
+
+```mermaid
+flowchart TD
+ C8["C8 apoiadores ✓"] --> F1
+ F1["Fase 1 — Filtro unificado
supporterUi + aggregate SQL"]
+ F1 --> F2["Fase 2 — contactSearchQuery + loader página
1× núcleos coordenador"]
+ F2 --> F3["Fase 3 — formActions detalhe apoiador
contrato message preservado"]
+ F3 --> F4["Fase 4 — fieldError nos componentes restantes"]
+
+ C7["C7 contactSearchQuery ✓"] -.-> F2
+```
+
+### Fase 1 — Núcleo de filtros de apoiadores
+
+- Extrair condições compartilhadas de `buildSupporterListWhere` e `buildAggregateSql` para `src/utilities/supporterListFilters.ts` (ou equivalente): `q`, telefone, `voteIntention`, `city`, `nucleus`, mais adaptadores `toPayloadWhere(state)` / `toAggregateSqlConditions(state, access)`.
+- Testes int: mesmo `SupporterListState` produz o mesmo conjunto filtrado na lista e no overview (fixture com `q`, cidade, núcleo, intenção).
+- Critério: mudança futura de filtro exige editar um só lugar.
+
+### Fase 2 — `contactSearchQuery` + loader da página
+
+- Em `buildAggregateSql` (e `buildSupporterListWhere` se ainda aceitar `q` curto): ignorar `q` quando `!isContactSearchQueryReady(q)` — alinhado a `ContactCombobox` / C7.
+- Criar `loadSupportersPageData` em `supporterPageData.ts`: uma chamada a `getCoordinatorNucleusIds` para `coordenador`, repassar IDs para list constraints (se viável) + overview + evitar prefetch duplicado na `page.tsx`.
+- Avaliar `React.cache` só como otimização secundária se o loader único não bastar.
+
+### Fase 3 — `apoiadores/[id]/formActions.ts`
+
+- Migrar `setSupporterVoteIntentionFormAction` / `removeSupporterDataFormAction` para `mapCampaignFormActionError` com `resolveBoundaryMessage` ou branches que mantêm `message` (não `fieldErrors`) onde `SupporterDetail` espera banner global.
+- Espelhar padrão de `apoiadores/novo/formActions.ts` (`getSupporterFormError`) onde aplicável.
+
+### Fase 4 — Componentes de form restantes
+
+- Substituir `errorFor` / `state.fieldErrors?.x?.[0]` por `fieldError` ou `errorProps` de `campaignFormFields.ts` em: `NucleusForm`, `ActionPlanForm`, `NucleusTerritoryFields`, `NucleusTerritoryAndZonesFields`, `VoteEstimateDialog`, `ActionPlanUpdateForm`, `LeadershipPrimaryContactAction`, `NucleusIntelligenceDialog` (lista do simplify 2026-07-19).
+- Opcional baixa prioridade: `planos/formActions` regex de unique → pre-check de título como em `nucleos/formActions` (`existingActionPlanState`).
+
+**Migration:** nenhuma prevista. Sem Consent novo.
+
+## Dependências
+
+- **Dura:** C8 Escala e DRY pós-C6 (merge) — código em `supporterListOverviewAggregate.ts`, `supporterPageData.ts`, `supporterUi.ts`, `campaignFormActionError.ts`.
+- **Suave:** C7 `contactSearchQuery` — já em `main`; Fase 2 wireia.
+- Reusa: `src/lib/contactSearchQuery.ts`, `getCoordinatorNucleusIds`, `campaignFormFields.ts`, padrão `loadSupporterListPageData`, plano C8 [escala-dry-pos-c6.md](escala-dry-pos-c6.md).
+
+## Não escopo
+
+- Reabrir C6/C8 (import bulk, token HMAC, `pg_trgm` migration, locks batch).
+- Unificar lista+overview numa única query SQL (só se medição de latency justificar).
+- Widen `PostgresTransactionDatabase` — follow-up de plataforma (assertion runtime do C8 cobre drift de colunas no bulk).
+- Helpers FormData/URL de território do actionPlan — C7 Fase 3 / [escala-dry-pos-c3.md](escala-dry-pos-c3.md).
+- GOTV / dia D — C5.
+
+## Referências
+
+- `docs/roadmap.md` — Trilha C, item C9; sequência Janela 2
+- [escala-dry-pos-c6.md](escala-dry-pos-c6.md) — C8 entregue; precedente do formato
+- [escala-dry-pos-c3.md](escala-dry-pos-c3.md) — `contactSearchQuery` (C7)
+- [escala-dry-pos-c2.md](escala-dry-pos-c2.md) — cadastro nominal C2/C6
+- `src/utilities/supporterUi.ts` — `buildSupporterListWhere`
+- `src/utilities/supporterListOverviewAggregate.ts` — `buildAggregateSql`, `resolveAccessConstraint`
+- `src/app/(campaign)/campanha/(app)/apoiadores/page.tsx` — prefetch / waterfall overview
+- `src/lib/contactSearchQuery.ts` — `isContactSearchQueryReady`
+- `src/app/(campaign)/campanha/(app)/apoiadores/[id]/formActions.ts` — ladder legado
+- `src/utilities/campaignFormFields.ts` — `fieldError`, `errorProps`
+- AGENTS.md — `Contact`, `overrideAccess: false`, naming inglês
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 0db61315..3e23b30f 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -1,6 +1,6 @@
# Roadmap — Teqo
-Atualizado em: 2026-07-19 (MVP + Ciclo 2 deployados; A2 entregue; A4 Baseline no produto + Gap vs 2022 implementado e mesclado em `main` (Fases 2–4 + simplify); A7 registrado — escala/DRY pós-A4 do `/simplify`; C2 engenharia pronta e mesclada em `main`; C3 Planos de Ação implementado e mesclado em `main`; C6 Escala e DRY pós-C2 implementado e mesclado em `main` (Fases 1–5 + simplify); C7 parcial 2026-07-19 (FormData território, contadores lista, locks, contactSearchQuery) — débitos pós-simplify registrados no plano; C8 registrado — escala/DRY pós-C6 do `/simplify`; B2 fundação de geometrias do mapa entregue; B5 registrado — escala/DRY pós-B2 do `/simplify`; **Trilha E registrada — E1–E5 mapa de projeção por município**, equiparar/superar as planilhas "Mapa de projeção de votos 2026" — [plano](plans/mapa-projecao-municipios.md))
+Atualizado em: 2026-07-19 (MVP + Ciclo 2 deployados; A2 entregue; A4 Baseline no produto + Gap vs 2022 implementado e mesclado em `main` (Fases 2–4 + simplify); A7 registrado — escala/DRY pós-A4 do `/simplify`; C2 engenharia pronta e mesclada em `main`; C3 Planos de Ação implementado e mesclado em `main`; C6 Escala e DRY pós-C2 implementado e mesclado em `main` (Fases 1–5 + simplify); C7 parcial 2026-07-19 (FormData território, contadores lista, locks, contactSearchQuery) — débitos pós-simplify registrados no plano; C8 Escala e DRY pós-C6 implementado (Fases 1–4 + duas passagens `/simplify`; pendente merge em `main`); C9 registrado — escala/DRY pós-C8 do `/simplify`; B2 fundação de geometrias do mapa entregue; B5 registrado — escala/DRY pós-B2 do `/simplify`; **Trilha E registrada — E1–E5 mapa de projeção por município**, equiparar/superar as planilhas "Mapa de projeção de votos 2026" — [plano](plans/mapa-projecao-municipios.md))
Registro canônico no repositório dos planos futuros e débitos conhecidos. Status operacional do ciclo atual de Núcleos fica em [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc); este arquivo lista o que ainda é futuro ou bloqueador, **em ordem de execução**, com dependências e paralelismo explícitos.
@@ -75,6 +75,9 @@ MVP de território + reporte implementado e enviado (ondas 1–8 + refactors de
- **C2 Cadastro nominal de apoiadores** — collection `supporter` (join `Contact`↔campanha, núcleo opcional), migration `20260718_222656_add_supporter` com `UNIQUE NULLS NOT DISTINCT (contact_id, nucleus_id)`, consent por chaves estáveis `apoiador-cadastro` / `apoiador-intencao-voto` via `campaignConsent.ts` genérico (`getConsentByKey` / `requireConsentByKey`, falha fechada), actions (create / intenção de voto / import CSV só `geral` / `removeSupporterData`), UI `/campanha/apoiadores` (lista+KPIs, ficha, wizard de import, kit mínimo `wa.me`). Telefone obrigatório no v1; `lideranca` sem acesso à área. Engenharia pronta e mesclada — produção com dados reais espera deploy (build Vercel aplica a migration) + Consent keys + aprovação jurídica (Onda 0). [Plano](plans/cadastro-nominal-apoiadores.md).
- **C3 Eventos / agenda de mobilização** — collection `actionPlan` + vertical `/campanha/planos` (lista com tabs Próximos/Todos/Realizados/Rascunhos, detalhe com tabs Visão geral/Tarefas/Atualizações, forms novo/editar), blocos "Próximos eventos" no overview de núcleos e no dashboard; `startAt` opcional só em rascunho (obrigatório ao sair de `rascunho`); access por `coordinators`/`leadership` (escopo `lideranca` só toggle `tasks.done` + append `updates`); transações via `withPayloadTransaction`; migration `20260718_222832_add_action_plan`. Sem `Consent` (dado interno de staff). [Plano](plans/eventos-agenda-mobilizacao.md).
- **C7 Escala e DRY pós-C3 — parcial (2026-07-19)** — primeira fatia: `campaignTerritoryFormData`, contadores `taskTotal`/`taskDoneCount` + migration `20260719_014906_action_plan_list_perf`, locks advisory + `select` mínimo em toggle/append, `contactSearchQuery` (mín. 2 chars), rename `CampaignTerritoryFields`. Débitos do `/simplify` 2026-07-19 (composição territorial, shell de contato, leituras por aba, short-circuit de hooks, `taskProgress` no detalhe) registrados no plano — Fases 1–2 e restante de 4–5 pendentes. [Plano](plans/escala-dry-pos-c3.md).
+- **C6 Escala e DRY pós-C2 — implementado e mesclado em `main` (2026-07-19)** — import bulk drizzle, token HMAC + `supporterImportBatch`, KPI aggregate, shells compartilhados (`campaignListUrl`, `CampaignListPagination`, `campaignFormFields`, `mapCampaignFormActionError`). Duas passagens `/simplify`; débitos maiores → C8. [Plano](plans/escala-dry-pos-c2.md).
+- **C8 Escala e DRY pós-C6 — implementado (2026-07-19; pendente merge)** — locks advisory em 1 RT, bulk com `.returning()` + leituras drizzle na txn, overview sem `COUNT(*)` redundante, migration `pg_trgm` em `contact`, `drizzleBulk.ts`, `getCoordinatorNucleusIds`, migração da maioria dos `formActions` para `mapCampaignFormActionError`. Duas passagens `/simplify` (pré- e pós-rebase com C7); débitos maiores → C9. [Plano](plans/escala-dry-pos-c6.md).
+- **C9 Escala e DRY pós-C8 registrado (2026-07-19)** — débitos do `/simplify` que não entraram no cleanup: filtro unificado Payload↔SQL, wire de `contactSearchQuery` no aggregate, loader único da página de apoiadores (1× núcleos do coordenador), `apoiadores/[id]/formActions` + componentes de form restantes. [Plano](plans/escala-dry-pos-c8.md).
### Ciclo 3 — Trilha E: mapa de projeção por município (registrado 2026-07-19)
@@ -104,6 +107,7 @@ Os designs gerados pelo UX Pilot para os próximos ciclos estão em [`docs/desig
| C3 Eventos / agenda de mobilização | `Planos-de-Acao`, `Novo-Plano-de-Acao` | [eventos-agenda-mobilizacao.md](plans/eventos-agenda-mobilizacao.md) |
| C7 Escala e DRY pós-C3 | — (sem design novo; reusa os de C3 + padrões de núcleos/C6) | [escala-dry-pos-c3.md](plans/escala-dry-pos-c3.md) |
| C8 Escala e DRY pós-C6 | — (sem design novo; reusa os de C2/C6) | [escala-dry-pos-c6.md](plans/escala-dry-pos-c6.md) |
+| C9 Escala e DRY pós-C8 | — (sem design novo; reusa os de C2/C6/C8) | [escala-dry-pos-c8.md](plans/escala-dry-pos-c8.md) |
| B5 Escala e DRY pós-B2 | — (sem design novo; reusa geometrias B2 + padrões de scripts CLI) | [escala-dry-pos-b2.md](plans/escala-dry-pos-b2.md) |
| D2 Notificações (sino) | `Notificacoes-PWA` (central de notificações) | [notifications.md](plans/notifications.md) |
| D1 PWA `/campanha` | `Notificacoes-PWA` (banner "Instale o app da campanha") | [pwa-campanha.md](plans/pwa-campanha.md) |
@@ -147,6 +151,7 @@ flowchart TD
C3["C3 Eventos / agenda de mobilização ✓"]
C7["C7 Escala e DRY pós-C3
(parcial: contadores/locks/FormData)"]
C8["C8 Escala e DRY pós-C6
(locks bulk / leituras / forms DRY)"]
+ C9["C9 Escala e DRY pós-C8
(filtros unificados / loader / forms)"]
C4["C4 Demandas"]
C5["C5 Operação dia D / GOTV
(proposto, a validar)"]
end
@@ -191,6 +196,7 @@ flowchart TD
C2 --> C5
C6 -.escala da base.-> C5
C6 --> C8
+ C8 --> C9
D1 --> D2
JUR -.chave de push.-> D2
A4 --> E2
@@ -203,7 +209,7 @@ flowchart TD
A7 -.mesmo loader do baseline.-> E2
```
-Itens sem seta de entrada (**paralelizáveis a qualquer momento**): E1, E3 e fill-ins (visitados recentemente, listas globais, reset de senha, higiene PascalCase). **Entregues e mesclados em `main` em 2026-07-18/19** (✓ no grafo): A1, A2, A3, A4, B1, B2, C1, C2, C3, C6 e D1 — ver "Ciclo 2" e "Ciclo 2+" acima. C2 segue com produção bloqueada pelo lote jurídico (Onda 0: Consent keys `apoiador-cadastro` / `apoiador-intencao-voto` + aprovação). Destravados por eles: A5/A6 (após A4 ✓), A7 (após A4 ✓; preferencialmente antes ou com A5/B3/E2), E2 (após A4 ✓), B3 (B1 ✓ + B2 ✓; suave: A4 ✓ para coroplético e B5 F1 para lazy load), B5 (após B2; preferencialmente com B3), C4 (A1 + C3 prontos), C7 (após C3; suave: C6 F2 ✓), C8 (merge de C6 ✓; cortável se a base nominal permanecer pequena) e D2 push (D1 pronto; falta a chave jurídica). E4 destravará após E1+E3 (campos precisam existir antes do import).
+Itens sem seta de entrada (**paralelizáveis a qualquer momento**): E1, E3 e fill-ins (visitados recentemente, listas globais, reset de senha, higiene PascalCase). **Entregues e mesclados em `main` em 2026-07-18/19** (✓ no grafo): A1, A2, A3, A4, B1, B2, C1, C2, C3, C6 e D1 — ver "Ciclo 2" e "Ciclo 2+" acima. C8 implementado (pendente merge). C2 segue com produção bloqueada pelo lote jurídico (Onda 0: Consent keys `apoiador-cadastro` / `apoiador-intencao-voto` + aprovação). Destravados por eles: A5/A6 (após A4 ✓), A7 (após A4 ✓; preferencialmente antes ou com A5/B3/E2), E2 (após A4 ✓), B3 (B1 ✓ + B2 ✓; suave: A4 ✓ para coroplético e B5 F1 para lazy load), B5 (após B2; preferencialmente com B3), C4 (A1 + C3 prontos), C7 (após C3; suave: C6 F2 ✓), C8 (merge de C6 ✓; cortável se a base nominal permanecer pequena), C9 (merge de C8; cortável se a base nominal permanecer pequena; Fases 3–4 DRY podem entrar isoladas) e D2 push (D1 pronto; falta a chave jurídica). E4 destravará após E1+E3 (campos precisam existir antes do import).
### Sequência de execução por janela do calendário
@@ -227,31 +233,32 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): E1, E3 e fil
| 9 | A4 Baseline no produto + insight Gap vs 2022 **(implementado e mesclado em `main` 2026-07-18 — Fases 2–4 + simplify)** | [detalhes](plans/baseline-eleitoral-tse.md) | A3 + B1 (suave: A2) | C2, C3, C6, C7, C8 |
| 10 | C3 Eventos / agenda de mobilização (`actionPlan`) **(implementado e mesclado em `main` 2026-07-18)** | [detalhes](plans/eventos-agenda-mobilizacao.md) | A1 | C2, A4, C6, C7, C8 |
| 11 | C7 Escala e DRY pós-C3 _(parcial 2026-07-19: FormData território, contadores lista, locks, `contactSearchQuery`; pendente: composição territorial, shell de contato, leituras por aba, short-circuit de hooks)_ | [detalhes](plans/escala-dry-pos-c3.md) | C3 (suave: C6 F2) | A4, C6, C8, E1+E3, E2 |
-| 12 | C8 Escala e DRY pós-C6 (locks bulk em 1 RT, leituras/`pg_trgm`, helpers drizzle, forms DRY) _(cortável se a base nominal permanecer pequena)_ | [detalhes](plans/escala-dry-pos-c6.md) | C6 (merge) | A4, C3, C7, E1+E3, E2 |
-| 13 | E1 Metas em cenários + prioridade + E3 dobradinhas/encaminhamentos manuais (migration compartilhada em `electoralNucleus`) | [detalhes](plans/mapa-projecao-municipios.md) | — | tudo da janela |
-| 14 | E2 Série histórica TSE 2014/2018 + tendência automática (seed por ano; sem migration) | [detalhes](plans/mapa-projecao-municipios.md) | A4 ✓ (suave: A7 F1 — mesmo loader) | E1+E3, C7, C8 |
+| 12 | C8 Escala e DRY pós-C6 (locks bulk em 1 RT, leituras/`pg_trgm`, helpers drizzle, forms DRY) **(implementado 2026-07-19 — Fases 1–4 + simplify; pendente merge)** _(cortável se a base nominal permanecer pequena)_ | [detalhes](plans/escala-dry-pos-c6.md) | C6 (merge) | A4, C3, C7, E1+E3, E2, C9 |
+| 13 | C9 Escala e DRY pós-C8 (filtro unificado Payload↔SQL, `contactSearchQuery` no aggregate, loader único `/apoiadores`, forms restantes) _(cortável se a base nominal permanecer pequena; Fases 3–4 DRY podem entrar isoladas)_ | [detalhes](plans/escala-dry-pos-c8.md) | C8 (merge) | A4, C3, C7, E1+E3, E2 |
+| 14 | E1 Metas em cenários + prioridade + E3 dobradinhas/encaminhamentos manuais (migration compartilhada em `electoralNucleus`) | [detalhes](plans/mapa-projecao-municipios.md) | — | tudo da janela |
+| 15 | E2 Série histórica TSE 2014/2018 + tendência automática (seed por ano; sem migration) | [detalhes](plans/mapa-projecao-municipios.md) | A4 ✓ (suave: A7 F1 — mesmo loader) | E1+E3, C7, C8, C9 |
**Janela 3 — 16/08 → set (campanha de rua): inteligência ampliada, visualização e engajamento.**
| Ordem | Item | Plano | Depende de | Paralelizável com |
| ----- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ---------------------- |
-| 15 | E4 Import único da planilha (`pnpm db:seed:mapa`; sem criar `Contact`/`leadership`) | [detalhes](plans/mapa-projecao-municipios.md) | E1 + E3 (suave: E2 — tendência visível na revisão) | A5, A7, B3, B5, C4, D2 |
-| 16 | A5 Insights derivados do baseline (5 itens, paralelizáveis entre si; conversão e classificação exigem limiares de produto) | [conversão](plans/insight-taxa-conversao.md) · [classificação](plans/insight-classificacao-territorial.md) · [alavancagem](plans/insight-alavancagem-chapa.md) · [mobilização](plans/insight-mobilizacao-brancos-nulos.md) · [competitiva](plans/insight-inteligencia-competitiva.md) | A4 | A7, B3, B5, C4, E4, D1 |
-| 17 | A7 Escala e DRY pós-A4 (agregar federal no detalhe, `cityCode` TSE, Alert `confirmed` + Progress) | [detalhes](plans/escala-dry-pos-a4.md) | A4 ✓ (suave: A5/B3/E2 amplificam o custo) | A5, B3, B5, C4, E4, D1 |
-| 18 | B2 Mapa Fase 1 (geometrias) **(entregue 2026-07-18)** + B3 Leaflet nas superfícies | [detalhes](plans/mapa-bahia-geometrias.md) | B3 ← B1 ✓ + B2 ✓ (suave: A4 coroplético; E1/E2 métricas; B5 F1 lazy geometrias) | A5, A7, B5, C4, E4, D1 |
-| 19 | B5 Escala e DRY pós-B2 (lazy geometrias + cache CLI) | [detalhes](plans/escala-dry-pos-b2.md) | B2 ✓ (F1 preferencialmente com B3) | A5, A7, B3, C4, E4, D1 |
-| 20 | C4 Demandas | [detalhes](plans/demandas-campanha.md) | A1 (suave: C3 para a relação `actionPlan`) | A5, A7, B3, B5, E4, D1 |
-| 21 | D1 PWA `/campanha` **(entregue antecipado em 2026-07-18)** | [detalhes](plans/pwa-campanha.md) | — | tudo |
-| 22 | D2 Notificações (push + sino) — sino não depende do PWA; push sim | [detalhes](plans/notifications.md) | D1 ✓ (push) + chave `campanha-notificacoes-push` do lote jurídico | A5, A7, B3, B5, C4, E4 |
+| 16 | E4 Import único da planilha (`pnpm db:seed:mapa`; sem criar `Contact`/`leadership`) | [detalhes](plans/mapa-projecao-municipios.md) | E1 + E3 (suave: E2 — tendência visível na revisão) | A5, A7, B3, B5, C4, D2 |
+| 17 | A5 Insights derivados do baseline (5 itens, paralelizáveis entre si; conversão e classificação exigem limiares de produto) | [conversão](plans/insight-taxa-conversao.md) · [classificação](plans/insight-classificacao-territorial.md) · [alavancagem](plans/insight-alavancagem-chapa.md) · [mobilização](plans/insight-mobilizacao-brancos-nulos.md) · [competitiva](plans/insight-inteligencia-competitiva.md) | A4 | A7, B3, B5, C4, E4, D1 |
+| 18 | A7 Escala e DRY pós-A4 (agregar federal no detalhe, `cityCode` TSE, Alert `confirmed` + Progress) | [detalhes](plans/escala-dry-pos-a4.md) | A4 ✓ (suave: A5/B3/E2 amplificam o custo) | A5, B3, B5, C4, E4, D1 |
+| 19 | B2 Mapa Fase 1 (geometrias) **(entregue 2026-07-18)** + B3 Leaflet nas superfícies | [detalhes](plans/mapa-bahia-geometrias.md) | B3 ← B1 ✓ + B2 ✓ (suave: A4 coroplético; E1/E2 métricas; B5 F1 lazy geometrias) | A5, A7, B5, C4, E4, D1 |
+| 20 | B5 Escala e DRY pós-B2 (lazy geometrias + cache CLI) | [detalhes](plans/escala-dry-pos-b2.md) | B2 ✓ (F1 preferencialmente com B3) | A5, A7, B3, C4, E4, D1 |
+| 21 | C4 Demandas | [detalhes](plans/demandas-campanha.md) | A1 (suave: C3 para a relação `actionPlan`) | A5, A7, B3, B5, E4, D1 |
+| 22 | D1 PWA `/campanha` **(entregue antecipado em 2026-07-18)** | [detalhes](plans/pwa-campanha.md) | — | tudo |
+| 23 | D2 Notificações (push + sino) — sino não depende do PWA; push sim | [detalhes](plans/notifications.md) | D1 ✓ (push) + chave `campanha-notificacoes-push` do lote jurídico | A5, A7, B3, B5, C4, E4 |
**Janela 4 — set → 04/10 (reta final): dobradinha, dia D e estabilização.**
| Ordem | Item | Plano | Depende de |
| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
-| 23 | A6 Insight dobradinha 2026 | [detalhes](plans/insight-dobradinha-2026.md) | A4 + TSE publicar candidaturas 2026 (externo, após 15/08) + taxonomia de alinhamento (produto) |
-| 24 | B4 Camada de zonas TSE no mapa | ciclo seguinte do [plano do mapa](plans/mapa-bahia-geometrias.md) | A2 + B3 |
-| 25 | C5 **Operação dia D / GOTV** _(item proposto nesta revisão — validar com produto)_: confirmação de comparecimento da base nominal em 04/10 (lista de apoiadores por núcleo/zona com marcação "confirmou que vai votar / votou", visão de cobertura para a coordenação). É o uso final da base construída em C2; a literatura de campanha trata a mobilização do dia como onde eleições apertadas se decidem. Sem plano ainda. | design: [`Dia-D-GOTV`](design-refs/latest/Dia-D-GOTV.png) | C2 (suave: C6 para escala da base) |
-| 26 | Congelamento: a partir de ~20/09, só correção de bug e dados; nada de migration arriscada perto do dia D. E5 (Salvador por bairro) fica registrado para depois da eleição, condicionado a decisão de produto. | — | — |
+| 24 | A6 Insight dobradinha 2026 | [detalhes](plans/insight-dobradinha-2026.md) | A4 + TSE publicar candidaturas 2026 (externo, após 15/08) + taxonomia de alinhamento (produto) |
+| 25 | B4 Camada de zonas TSE no mapa | ciclo seguinte do [plano do mapa](plans/mapa-bahia-geometrias.md) | A2 + B3 |
+| 26 | C5 **Operação dia D / GOTV** _(item proposto nesta revisão — validar com produto)_: confirmação de comparecimento da base nominal em 04/10 (lista de apoiadores por núcleo/zona com marcação "confirmou que vai votar / votou", visão de cobertura para a coordenação). É o uso final da base construída em C2; a literatura de campanha trata a mobilização do dia como onde eleições apertadas se decidem. Sem plano ainda. | design: [`Dia-D-GOTV`](design-refs/latest/Dia-D-GOTV.png) | C2 (suave: C6 para escala da base) |
+| 27 | Congelamento: a partir de ~20/09, só correção de bug e dados; nada de migration arriscada perto do dia D. E5 (Salvador por bairro) fica registrado para depois da eleição, condicionado a decisão de produto. | — | — |
**Fill-ins (qualquer janela, quando houver folga; nenhum bloqueia nada):**
@@ -260,7 +267,7 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): E1, E3 e fil
- **Reset de senha self-service + foto de perfil** (UX adiada do ciclo 1). _(plano MVP)_
- **Higiene de código:** varredura PascalCase dos componentes legados. _(notebook Núcleos)_
-**Cortes seguros se o prazo apertar** (nesta ordem): E5 Salvador por bairro (já registrado como futuro), B4 camada de zonas, B5 F2 (cache CLI — duplicação pequena), E4 import da planilha (os campos E1/E3 podem ser preenchidos à mão pela coordenação; o import só acelera), A7 F2–F3 (`cityCode` / Alert DRY — só se a geografia real dos núcleos permanecer 1–2 municípios), E2 série 2014/2018 (reduzível a só 2018 se o formato TSE de 2014 atrasar; a tendência vira 2 pontos), B3 mapa Leaflet (preferir levar B5 F1 lazy load junto se B3 entrar), C4 demandas, D2 push (mantendo o sino), C6 fases 3–5 (import bulk / preview token / KPI — só se a base nominal permanecer pequena), C7 fases 4–5 restantes (leituras por aba / short-circuit de hooks / feed O(n) — só se a agenda permanecer pequena; contadores + locks já entregues), C8 (escala/DRY pós-C6 — só se a base nominal permanecer pequena; Fases 3–4 DRY podem entrar isoladas), fill-ins (D1 PWA já entregue, saiu da lista de cortes). Preferir manter C6 fases 1–2, C7 fases 1–2, C8 fases 3–4 (DRY de helpers/forms) e **A7 F1** (agregar federal no detalhe) se A5/B3/E2 forem entrar — o custo de I/O do loader do A4 é herdado por eles. **Não cortáveis:** Onda 0 (jurídico/Consent), C2 cadastro de apoiadores, C3 eventos/agenda, A4 baseline + gap, **E1+E3 metas/prioridade/estratégia manual** — são respectivamente o risco legal, a base de dados, a operação da propaganda, o instrumento de alocação de esforço e a paridade mínima com a planilha que a campanha já usa.
+**Cortes seguros se o prazo apertar** (nesta ordem): E5 Salvador por bairro (já registrado como futuro), B4 camada de zonas, B5 F2 (cache CLI — duplicação pequena), E4 import da planilha (os campos E1/E3 podem ser preenchidos à mão pela coordenação; o import só acelera), A7 F2–F3 (`cityCode` / Alert DRY — só se a geografia real dos núcleos permanecer 1–2 municípios), E2 série 2014/2018 (reduzível a só 2018 se o formato TSE de 2014 atrasar; a tendência vira 2 pontos), B3 mapa Leaflet (preferir levar B5 F1 lazy load junto se B3 entrar), C4 demandas, D2 push (mantendo o sino), C6 fases 3–5 (import bulk / preview token / KPI — só se a base nominal permanecer pequena), C7 fases 4–5 restantes (leituras por aba / short-circuit de hooks / feed O(n) — só se a agenda permanecer pequena; contadores + locks já entregues), C8 (escala/DRY pós-C6 — só se a base nominal permanecer pequena; Fases 3–4 DRY podem entrar isoladas), C9 (escala/DRY pós-C8 — só se a base nominal permanecer pequena; Fases 3–4 DRY de forms podem entrar isoladas), fill-ins (D1 PWA já entregue, saiu da lista de cortes). Preferir manter C6 fases 1–2, C7 fases 1–2, C8 fases 1–2 (perf import/lista), C9 Fase 1 (filtro unificado — evita drift KPI/lista) e **A7 F1** (agregar federal no detalhe) se A5/B3/E2 forem entrar — o custo de I/O do loader do A4 é herdado por eles. **Não cortáveis:** Onda 0 (jurídico/Consent), C2 cadastro de apoiadores, C3 eventos/agenda, A4 baseline + gap, **E1+E3 metas/prioridade/estratégia manual** — são respectivamente o risco legal, a base de dados, a operação da propaganda, o instrumento de alocação de esforço e a paridade mínima com a planilha que a campanha já usa.
### Itens consolidados/removidos nesta revisão (2026-07-17)
diff --git a/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx b/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx
index 703d6570..d3f673c7 100644
--- a/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx
+++ b/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx
@@ -18,7 +18,7 @@ import {
EmptyMedia,
EmptyTitle,
} from '@/components/ui/Empty'
-import { isCampaignGeneral } from '@/utilities/campaignAccess'
+import { isCampaignGeneral, getCoordinatorNucleusIds } from '@/utilities/campaignAccess'
import { getCampaignUser } from '@/utilities/campaignAuth'
import {
loadAccessibleNucleusOptions,
@@ -31,7 +31,6 @@ import {
buildSupporterListHref,
canAccessSupporterArea,
getSupporterScopeLabel,
- resolveSupporterListUrl,
} from '@/utilities/supporterUi'
type SupportersPageProps = {
@@ -45,14 +44,24 @@ export default async function SupportersPage({ searchParams }: SupportersPagePro
if (!canAccessSupporterArea(user.role)) redirect('/campanha')
const rawSearchParams = await searchParams
- const provisional = resolveSupporterListUrl(rawSearchParams)
- const [{ result, state, redirectHref }, overview, nucleusOptions] = await Promise.all([
- loadSupporterListPageData(payload, user, rawSearchParams),
- loadSupporterListOverviewData(payload, user, provisional.state),
- loadAccessibleNucleusOptions(payload, user),
- ])
+ const [{ result, state, redirectHref }, nucleusOptions, coordinatorNucleusIds] =
+ await Promise.all([
+ loadSupporterListPageData(payload, user, rawSearchParams),
+ loadAccessibleNucleusOptions(payload, user),
+ user.role === 'coordenador'
+ ? getCoordinatorNucleusIds(payload, user.id)
+ : Promise.resolve(undefined),
+ ])
if (redirectHref) redirect(redirectHref)
+ const overview = await loadSupporterListOverviewData(
+ payload,
+ user,
+ state,
+ result.totalDocs,
+ coordinatorNucleusIds,
+ )
+
return (