Preserve pledge history, type field signals, link plan demands, and record immutable ex-ante allocation decisions.
15 KiB
Escala e DRY pós-C7 (planos de ação / agenda)
Status: registrado no roadmap (fases pendentes) Atualizado em: 2026-07-24 (extensão pós-C12 absorvida) Item do roadmap: docs/roadmap.md (Trilha C, item C11) Appetite da extensão pós-C12: ~0,5–1 dia eng, sem migration Impeccable: B — ajustes encaixados nos forms/detalhe existentes Responsável: —
Contexto
O C7 (escala-dry-pos-c3.md) entregou composição territorial (useCampaignTerritoryFieldsState + CampaignTerritoryCoreFields), shell AsyncSearchCombobox (+ ContactCombobox / PrimaryContactCombobox / LeadershipCombobox), leituras por aba no detalhe (getActionPlanDetailSelect + taskProgress + batch de autores na aba Atualizações), typeahead de liderança (searchActionPlanLeadershipOptions), short-circuit de hooks em toggle/append (context.mutationKind) e cleanup do /simplify (depth overview/tasks, searchRef, fieldError, remoção de exports mortos).
Duas passagens /simplify (pré- e pós-rebase com main/C8–C9) marcaram como importantes e maiores que cleanup os follow-ups abaixo. Sem registro, planos com feed longo degradam leitura/append; loaders do detalhe ficam fragmentados em relação ao padrão de núcleos; e hot paths de edição continuam O(n) no array embutido.
- Feed de atualizações O(n) (leitura + escrita). A aba Atualizações carrega o array
updatesinteiro (corpos até 4 000 chars) sem limite/cursor;appendActionPlanUpdateRecordfaz read-modify-write do array completo. Escala linear com o tamanho do feed — principal cliff de performance da vertical de agenda. - Toggle de tarefa O(n).
toggleActionPlanTaskRecordainda fazfindByID+updatecom o arraytasksinteiro (locks + short-circuit de hooks ajudam, mas o RMW permanece). - Selects do detalhe over-fetch em tasks/updates.
actionPlanDetailContextSelectincluicoordinators,responsible,createdBy,description, etc. nas abas que só precisam de header + conteúdo da aba — hidratação extra emdepth: 1. - Loader do detalhe fragmentado.
planos/[slug]/page.tsxencadeiaresolveAccessibleActionPlanContext+getActionPlanDetailPageData; núcleos já usam orquestrador único (loadNucleusDetailPageData/loadNucleusActiveTabPageData). - Batch de nomes de autor duplicado.
loadActionPlanUpdateAuthorNamesemactionPlanDetailPageData.tsrepete o padrão denucleusUpdatePageData/ overview de núcleos (campaignUserid in→Map<id, name>). - Form de edição hidrata todas as tarefas.
getActionPlanEditPageDatausaactionPlanFormSelect+depth: 1sempre — latência cresce com contagem de tarefas/responsáveis. serializeTasksno hidden input a cada keystroke.ActionPlanTaskFieldschamaJSON.stringifydo array completo em todo render enquanto o usuário edita tarefas.- Typeahead sem índice dedicado na busca de liderança.
searchActionPlanLeadershipOptionsusacontainsemcontact.*aninhado;pg_trgmemcontactjá existe (C8) mas a query de liderança ainda não reutiliza o padrão otimizado de apoiadores. - Lista de coordenadores sem paginação no form.
getEligibleNucleusCoordinatorOptionscompagination: falseem novo/editar — O(staff) por page view (aceitável no MVP, débito se o time crescer). mutationKindsem tipo compartilhado. Magic strings'taskToggle' | 'appendUpdate'em actions eActionPlan.ts— risco de drift.- DRY menor:
searchContactComboboxOptionsainda inline emcontactSearchActions.ts(liderança já emutilities/); label de liderança duplicada entreactionPlanLeadershipOptionsetoActionPlanFormViewModel;firstValuelocal emactionPlanDetailTabUi.ts(já existe emcampaignListUrl.ts); loader combinado da lista/campanha/planos(precedente:loadSupportersPageDatano C9). - Opções de plano sem limite na nova demanda (C12).
loadActionPlanOptionsusapagination: falsee serializa todos os planos acessíveis noDemandForm; custo de RSC/hidratação/DOM cresce com a agenda. - Demandas vinculadas sem paginação no detalhe do plano (C12). A visão geral carrega e renderiza todas as demandas para somar custo e listar vínculos; aceitável no volume atual, mas sem teto.
Explicitamente fora (revisores pediram skip no simplify / já coberto em outro item):
errorPropsnos forms — C10 / escala-dry-pos-c9.md Fase 3 (incluiActionPlanForm,NucleusTerritoryFields,NucleusTerritoryAndZonesFields).pg_trgmemcontact(migration) — C8 entregue; este plano só cobre adoção do padrão de busca na query de liderança se necessário.- Unificar
validateCampaignTerritoryFormData↔createCampaignTerritoryValidationHook— tipos de erro diferentes; permanece em escala-dry-pos-c3.md §Não escopo. - Renomear
NucleusTerritoryFields.tsx→CampaignTerritoryFields.tsx— fricção de descoberta; fill-in, não bloqueia escala. - View-model dedup form ↔ detalhe — refactor grande; só se surgir terceiro consumidor.
- Testes E2E da vertical — fora do escopo deste item de escala/DRY.
- Serialização de
demandsJsona cada tecla — máximo de 20 drafts torna o custo limitado; reavaliar somente se houver input lag medido. - Bulk de demandas criadas com o plano — manter Local API sequencial para preservar hooks/access/slug; reavaliar se p95 da transação passar de 500 ms.
- Helper compartilhado create/update para drafts — só extrair quando surgir o 3º call site; dois loops curtos não justificam uma abstração.
Objetivos
- Agenda (
/campanha/planos) permanece responsiva com dezenas de atualizações e tarefas por plano — medir antes de migration; feed append-only só se o volume justificar. - Detalhe e edição seguem o mesmo padrão de loaders que núcleos (orquestrador + tab data).
- Hot paths de mutação (toggle tarefa, append update) minimizam bytes lidos/escritos sem quebrar locks transacionais nem RBAC.
- Seletores/listas do vínculo plano↔demanda permanecem limitados em payload e DOM quando a agenda crescer.
- Guardrails:
overrideAccess: falsecomuser; sem novoConsent; migrations só na Fase 1 condicional (actionPlanUpdate).
Decisões travadas
- Um item C11, cinco fases ordenadas. Mesmo racional de A7/B5/C8–C10: um ID de roadmap, PRs por fase. Ordem: feed condicional (maior cliff) → loaders/selects (I/O) → mutações/form hot path → utilities DRY → fill-ins.
- Dependência dura de C7 mergeado em
main(código de abas, combobox, hooks). Não reabre Fases 1–5 do C7. - Fase 1 (feed) é condicional. Só migration + collection
actionPlanUpdateapós evidência (ex.: planos com >N updates ou latência medida em produção/staging). Até lá: documentar limite soft e monitorar. - Fase 1 condicional respeita congelamento ~20/09 — se entrar perto da reta final, adiar para pós-eleição; Fases 2–4 não exigem migration.
- Cortável se a agenda permanecer pequena (poucos planos, feeds curtos). Fase 1 é a mais cortável (e a única com migration). Fases 2–3 são as mais valiosas se houver uso real da agenda antes de 16/08.
- i18n e naming (AGENTS.md): identificadores em inglês (
loadActionPlanDetailPageData,loadCampaignUserNamesById,ACTION_PLAN_MUTATION_KIND), strings visíveis em pt-BR inalteradas. - Fase 6 pós-C12 é condicional e sem schema. Opções: manter listas integrais | picker remoto + paginação | data-grid. Recomendação: picker remoto filtrado por município e lista paginada/limitada no card, só quando houver volume real; rejeitado: data-grid ou bulk SQL que contorne hooks.
Questões em aberto
- Fase 1: threshold para extrair
actionPlanUpdate? Recomendação: 50 updates por plano ou p95 de append >500 ms em staging; até lá manter array embutido e só paginar leitura (limit + cursor) sem nova collection se bastar. - Fase 1: paginação só na leitura vs collection append-only? Recomendação: primeiro limitar/paginar leitura na aba Atualizações (sem migration); collection só se append RMW virar gargalo medido.
- Fase 3: SQL direto no toggle de tarefa? Recomendação: não na primeira iteração; manter Local API + locks; reavaliar com drizzle na txn só se profiling mostrar necessidade (mesmo trade-off do C8).
- Fase 2:
loadActionPlanDetailPageDataretorna{ context, view }ou sóview? Recomendação:{ context, view }espelhando núcleos; página deixa de encadear dois awaits públicos.
Abordagem proposta
flowchart TD
C7["C7 Fases 1–5 ✓"] --> F2
F2["Fase 2 — Loaders + selects por aba<br/>+ loadCampaignUserNamesById"]
F2 --> F3["Fase 3 — Hot path form/mutação<br/>(serializeTasks, mutationKind)"]
F2 --> F4["Fase 4 — Utilities DRY<br/>(contact search, labels, firstValue)"]
F1["Fase 1 — Feed escala<br/>(paginação / actionPlanUpdate)"]
C7 --> F1
F1 -.condicional volume.-> F5["Fase 5 — Loader lista planos"]
F3 -.opcional.-> F5
C12["C12 Registro-fundação ✓"] --> F6["Fase 6 — Plano ↔ demandas<br/>picker + paginação condicional"]
Fase 1 — Escala do feed de atualizações (condicional)
- 1a (sem migration): limitar leitura na aba Atualizações (
limit+ ordenação porcreatedAtdesc); UI “carregar mais”;getActionPlanDetailSelectpode omitir corpos antigos até expandir. - 1b (com migration, se medido): collection
actionPlanUpdateappend-only + relaçãoactionPlan↔ updates; migrar dados existentes;appendActionPlanUpdateRecordviracreatena collection filha; manter snapshot/embed mínimo no pai se necessário para contadores. - Teste int: append concorrente + leitura paginada; não regressar locks advisory.
Fase 2 — Loaders, batching e selects por aba
loadCampaignUserNamesById(payload, user, ids)emsrc/utilities/— reutilizar emactionPlanDetailPageData.ts,nucleusUpdatePageData.tse overview de núcleos.loadActionPlanDetailPageData(payload, user, slug, activeTab)→{ context, view }unificandoresolveAccessibleActionPlanContext+getActionPlanDetailPageData.- Refinar
getActionPlanDetailSelect: header mínimo compartilhado + campos por aba (tasks:tasks+ responsáveis das tarefas; updates:updatesapenas; overview: relações +taskDoneCount/taskTotal). - Opcional:
getActionPlanEditPageDatacomselect/depthpor seção (tarefas lazy) — só se edição ficar lenta em staging.
Fase 3 — Hot paths de mutação e form de tarefas
- Constante tipada
ACTION_PLAN_MUTATION_KINDcompartilhada entreactionPlan.tsactions eActionPlan.tshooks. ActionPlanTaskFields: evitarJSON.stringifyem todo render — atualizar hiddentasksJsonsó no submit ou com debounce/useDeferredValue.- Documentar limite soft de tarefas por plano se toggle continuar RMW.
Fase 4 — Utilities e DRY menor
searchContactComboboxOptions(payload, user, query)emutilities/;contactSearchActions.tsfica auth wrapper (paridade com liderança).actionPlanLeadershipLabel(leadership)único para search +toActionPlanFormViewModel.actionPlanDetailTabUi.tsimportafirstValuedecampaignListUrl.ts.- Avaliar índice/query de liderança alinhada ao padrão C8/C9 (
contactSearchQuery+pg_trgmonde aplicável).
Fase 5 (opcional) — Loader da lista de planos
loadActionPlansPageDatacombinando canonical URL + list (precedenteloadSupportersPageData).- Entra só se a lista ganhar filtros/overview pesados; cortável.
Fase 6 (condicional) — Escala do vínculo plano ↔ demandas
- Trocar
loadActionPlanOptions(... pagination: false)por picker server-backed após o município ser escolhido; consulta seleciona somenteid,titleemunicipality. - Paginar ou limitar o card “Demandas vinculadas” no detalhe do plano, preservando a soma de custo em aggregate separado e link para a lista filtrada.
- Gatilhos: ≥100 planos acessíveis a um ator, ≥50 demandas num plano, payload RSC >100 KB ou latência p95 >500 ms.
- Sem migration; reusar
AsyncSearchCombobox, access existente e o shell de paginação. Não criar segundo sistema de picker/lista.
Já resolvido no simplify/critique (não reabrir)
- Snapshot JSON da migration C12 gerado e verificado sem schema drift.
- Schema dos drafts derivado de
campaignDemandCreateSchema; opções de sinal derivadas dos enums canônicos. - Casts/estado/nullabilidade redundantes removidos; query params e access reutilizam helpers/política existentes.
- Município do plano cacheado em
req.contextdurante creates aninhados, removendo leituras repetidas na mesma transação.
Dados → decisão → apresentação
Dados: N/A — esta extensão reduz payload, I/O e DOM; não cria métrica nem nova apresentação analítica.
Migration: somente Fase 1b (condicional). Demais fases sem schema.
Dependências
- Dura: C7 Escala e DRY pós-C3 — entregue.
- Dura da Fase 6: C12 Registro-fundação — entregue em código.
- Suave: C8
pg_trgmemcontact— já emmain; acelera typeahead se Fase 4 adotar o padrão. - Suave: C10
errorProps— paralelo; não bloqueia C11 (escopos distintos). - Reuso:
src/utilities/actionPlanPageData.ts,actionPlanDetailPageData.ts,actionPlanViewModels.ts,src/app/(campaign)/campanha/actions/actionPlan.ts,src/collections/ActionPlan.ts, padrãoloadNucleusDetailPageData,campaignListUrl.ts,contactSearchQuery.ts.
Não escopo
errorPropsnos forms — C10 / escala-dry-pos-c9.md.- Demandas / relação
actionPlanopcional — C4. - Notificações de agenda — D2.
- Calendário visual / mapa de eventos — fora do horizonte pré-16/08.
- Unificação FormData ↔ hook de território Payload — escala-dry-pos-c3.md §Não escopo.
- Coordinator list paginada — só entra se staff > ~50; até lá fill-in.
Referências
docs/roadmap.md— Trilha C, item C11; sequência Janela 2- escala-dry-pos-c3.md — C7 entregue; feed O(n) registrado aqui
- escala-dry-pos-c6.md — precedente C8 (
pg_trgm) - escala-dry-pos-c8.md — precedente C9/C10
src/utilities/actionPlanDetailPageData.ts— batch autores + view por abasrc/utilities/actionPlanPageData.ts— depth/select por abasrc/app/(campaign)/campanha/actions/actionPlan.ts— toggle/append RMWsrc/components/campaign/ActionPlanTaskFields.tsx—serializeTaskssrc/utilities/actionPlanLeadershipOptions.ts— typeaheadsrc/app/(campaign)/campanha/(app)/planos/contactSearchActions.ts— server actions de busca- AGENTS.md — transações,
overrideAccess: false, naming
Simplify (2026-07-19)
Passagens /simplify sobre o diff do C7 (incl. pós-rebase com C8–C9/Onda 0) aplicaram limpezas pontuais (fieldError, LeadershipCombobox, searchRef, depth overview, exports mortos, skip buildTerritorySuggestions com override). Débitos maiores registrados neste plano como item C11 do roadmap.