The "Municípios" column was dead text; it is now chips that navigate to the
município, with a territory chip when a leadership covers a whole TI, and it is
editable in place. What picks inline vs. Drawer is the `pointer:` media query,
not viewport width: on a fine pointer the editor is always mounted (the "×" sits
at the chip's corner, revealed on hover/focus) and on a coarse pointer a tap
anywhere in the cell opens a Drawer with 44 px targets.
`AdvisorMunicipalityCell` is promoted to `shared/MunicipalityPortfolioCell` and
/campanha/assessores migrates onto it in the same change, so the second call site
is born with the first. One batch action only; the single-id advisor twin is
deleted with its int tests migrated. The anti-goal on always-mounted list inputs
is amended for chip-based relations on fine pointers, not reopened.
Also fixes a latent bug: `CampaignTableHead`'s `description` seam broke any RSC
caller, because its two helpers lived in a 'use client' module.
`municipality.engagementLevel` (staff-only, no default) records what the
campaign decided to invest in a município, next to the E8/E10 metrics that
describe what it knows. Moving it is a reallocation decision, so only
unrestricted staff may, every move carries a rationale plus the signals that
would reverse it, and the write files an `allocationDecision` in the same
transaction under an advisory lock taken before the current level is read —
otherwise the snapshot's `from` is a guess. The enum gains `movimento`: a level
move is neither `aceita` nor `descarta`.
No backfill. Null is "Sem nível", and filtering by it is the triage queue.
That is also why the list sorts `nivel` in memory rather than in the query:
Postgres puts NULLs first on DESC, so a native sort would open "N4 primeiro"
on a page of municípios nobody has decided about yet. The filter stays native.
The hysteresis rules from research §6.8 are pure and client-safe, so the
Popover explains a block before submitting and the server re-runs them anyway;
a blocked move answers 409 with typed violations and the override is recorded
with exactly the rules it contradicted.
Tendência, Assessores and Votos estimados now share one container
(CampaignCellEditOverlay: Popover on md+, bottom Drawer below), the card drops
to the table's density, and the whole card navigates through a stretched link
instead of a second "Abrir município" button.
- Popover + NativeSelect sempre montado na coluna Status de
/campanha/liderancas, sem toggle de modo de edição
- Auto-save (~150ms) via POST /campanha/liderancas/support-status,
reusando isSameOriginRequest/campaignJsonMutationRoute (B24/B27),
sem revalidatePath
- Extrai leadershipStaffEditSafeMessages (adotado também em
liderancas/formActions.ts e nova/formActions.ts) e
utilities/leadershipLabels.ts (3º call site de supportStatusLabels,
agora fonte única também para SupportStatusBadge)
- Novo e2e tests/e2e/campaignLeaderships.e2e.spec.ts; setup.e2e.spec.ts
prewarma as rotas novas
- Fecha o Adiado com gatilho do B29; docs/plans, notebook e roadmap
atualizados
Gate: tsc/lint/format/knip(P3 pré-existente)/check:cycles/unit+int/e2e
verdes; two /simplify passes (labels/estado duplicado/isSupportStatus;
flush-on-close ao fechar o popover, comentário órfão de summary/
campos summary mortos removidos de SupportStatusBadge).
Salvador era um polígono só carregando os votos da cidade inteira, então o
coroplético não conseguia responder "qual pedaço de Salvador está fraco / é
meu". As 19 ZE ganham polígono próprio, dissolvido da malha de bairros do IBGE
(Censo 2022) pela lista de circunscrição da RA 02/2017 do TRE-BA, e o mapa passa
a pintar por map key (slug para as zonas, ibgeCode para o resto) em vez de
codarea. O polígono municipal de Salvador continua desenhado por baixo, sem dado
e sem interação, para não abrir buraco no contorno do estado.
Aproximação declarada na copy e no cabeçalho do script: o TRE publica a
circunscrição como lista de bairros, então o bairro é o átomo geométrico —
Periperi, que a resolução corta na Rua das Pedrinhas, vai inteiro para a ZE 17.
competitiveRank segue por cidade (o artefato TSE só tem federalRankByIbgeCode).
Sem migration, sem collection, sem action, sem Consent.
Terceira rodada de /simplify sobre o B14, com dois enunciados errados corrigidos:
- A propriedade da fronteira compartilhada NAO vem da ordem do `.find()`, e sim
da convencao meio-aberta do ray casting (`point.lng < crossingLng`): a aresta
pertence ao poligono cujo interior fica a leste (ao norte, se horizontal). Tres
comentarios diziam o contrario, e o do unit test dizia "oeste" enquanto
afirmava `east`.
- O plano dizia que o `Promise.all` esconde o decode da malha atras do fix de
GPS. Nao esconde: o decode roda no topo do modulo, quando o chunk executa. O
paralelo continua certo por outro motivo (decode ~2 ms; sequencial jogaria os
42 KB no caminho critico quando o fix vem do cache).
Simplificacoes: `unsupported` virou estado proprio, entao `failureCopy` deixa de
ser anulavel e caem duas leituras defensivas inalcancaveis; "Voce esta em " era
escrito duas vezes (frase sr-only + JSX), o que anulava justamente a invariante
que o comentario alegava; `zoneCityHrefs` virou `Partial` porque a busca falha
para os 416 municipios sem zona e o guard existia sem o tipo exigir;
`MunicipalityForIbgeIndex` era duplicata campo-a-campo de `AccessibleMunicipality`;
`kmFormatter` duplicava `oneDecimalFormatter`; `RecentlyVisitedCard` carregava
tres props que nenhum caller passa (dez condicionais mortas); e o bbox de feature
estava escrito em dois specs (agora `tests/helpers/featureBounds.ts`, compartilhado
por vitest e Playwright).
Copy: fora da Bahia sem nada por perto dizia a mesma coisa em duas frases e nunca
dizia que voce estava fora do estado. Agora o headline afirma o fato e o detalhe
reusa "Nenhum municipio da sua carteira por perto.".
Tambem limpa um achado de knip herdado do main (`MunicipalityPoliticalTrendViewModel`
exportado sem uso) e registra no plano o caminho afiado do debito de payload RSC
mais dois adiados com gatilho.
Card "Onde estou" no dashboard staff: point-in-polygon contra a malha
municipal que o painel do mapa ja carrega, sem artefato de centroides,
sem migration, sem collection, sem server action e sem Consent -- a
posicao e lida por navigator.geolocation e casada no browser, nunca sai
do aparelho.
- src/lib/municipalityProximity.ts: haversine, ray casting, centroide
ponderado por area e uniao discriminada
(inScope | zoneCity | outOfScope | outsideBahia)
- src/utilities/campaignGeolocation.ts: flag de sessao, leitura guardada
de permissao e requestCurrentPosition com falha tipada
- Salvador nao ganha ZE chutada: link para a lista filtrada, com href
serializado no servidor (importar o serializador canonico no cliente
custava 21 kB de First Load JS)
- fallback fora da carteira ordenado por centroide, teto de 150 km
- fix grosseiro (>10 km) mostra o resultado com ressalva de precisao
/campanha First Load JS: 265 -> 268 kB.
Replaces the checkbox-list + "Salvar assessores" button in the
Assessors popover (/campanha/municipios) with removable chips + a
Command combobox, delta-based auto-save (one advisor add/remove per
request, never the whole array), and optimistic UI with request
sequencing to resolve out-of-order responses correctly.
- New POST /campanha/municipios/advisors JSON route (mirrors
expected-votes/), backed by setMunicipalityAdvisorMembership(Record)
in actions/municipality.ts (text advisory lock + transaction).
- nextAdvisorIdsAfterMembership moved to
src/lib/municipalityAdvisorMembership.ts with unit tests.
- Shared isSameOriginRequest (src/utilities/sameOriginRequest.ts) and
uniqueRelationshipIds (src/utilities/relationship.ts) helpers,
de-duplicating the same-origin CSRF check and the
Set+map(relationshipId)+filter pattern across advisor call sites.
- MunicipalityListAdvisorsControl.tsx rewritten: chips, Command
combobox, per-item pending affordance, rollback on error, and a
request-sequence guard so only the most recently sent delta's
response is adopted when concurrent optimistic updates settle.
- e2e coverage for the combobox auto-save flow; setup prewarms the new
POST-only route to avoid Playwright dev-server HMR aborting the
first request.
- Plan and roadmap marked as delivered (B27).
Collection actionPlan -> activity (labels "Atividade"/"Atividades"), campo
campaignDemand.actionPlan -> activity, rota /campanha/planos ->
/campanha/atividades (criacao em atividades/nova), sidebar "Atividades" e
rotulos alinhados a entidade ("Tipo de atividade", "Origem da atividade",
"Resultado da atividade"). Valores de enum kind/status/origin inalterados.
Schema movido por migration data-preserving escrita a mao
(20260725_213000_rename_action_plan_to_activity): 4 tabelas, 3 enums, 2
colunas, 39 indices (4 pkeys inclusos), 14 FKs e 2 sequences, cada statement
guardado na existencia do objeto de origem, com down() simetrico. O gerador do
Payload emitiria DROP+CREATE e nao ve o indice parcial escrito a mao
action_plan_upcoming_start_at_idx, entao a lista veio de
pg_indexes/pg_constraint/pg_type/pg_sequences e o snapshot .json foi derivado
do anterior por transformacao textual -- validado por migrate:create __probe
sem mudanca de schema. A mesma migration alinha os 8 indices fosseis %plaza%
que a remodelagem de municipios deixou (renomeou as colunas plaza_id, nao os
indices).
Guard: o describe "banned campaign terminology" de
codebaseConventions.unit.spec.ts virou tabela de termos varrendo src+tests+
scripts (migrations fora -- historia congelada), banindo os compostos
actionPlan, a copy "plano de acao" e a rota antiga, mas nao plan/plano isolado
(colidiria com os planos de assinatura da Fase 2). Allowlist: o proprio spec,
scripts/generate-remodel-municipalities-migration.mjs e o comentario historico
em campaignMigrationReconciliation.int.spec.ts.
Plano: docs/plans/renomear-plano-acao-para-atividade.md
- ~70 strings visiveis + labels de admin (headers de coluna, APIErrors, empty states, dashboard, mapa, formularios, filtros) com concordancia de genero corrigida
- apoiadores desambiguado: cidade do contato vira 'Cidade'; a unidade operacional assume 'Municipio' (filtro, coluna, ficha)
- comentarios de codigo atualizados; payload-types regenerado
- fora por decisao D6: textos provisorios de consent do Onda 0 (lote juridico) e migrations congeladas
- e2e do plano de acao acompanha o label ('Municipio *')
A lista de municípios vira a fila de decisão, sem rota nova, sem coluna nova
e sem migration.
Fase 0 corrige a meta sintética do E8, que repartia a meta estadual
proporcionalmente só ao teto do campo e ignorava a votação própria (meta 2.911
para Vitória da Conquista, de 5.005 votos em 2022; 813 para Campo Formoso, de
47). `deriveSuggestedGoalsByScenario` substitui `decomposeStateGoal` e ancora a
meta na votação de 2022 por cenário — pessimista = base × (1 − margin), central
= base, otimista = base × (stateGoal ÷ Σ base), com clamp. `goalCoverage` passa
a receber `SuggestedGoalByScenario`, deixando `expectedVotes[S] ??
suggestedGoal[S]` simétrico. Só descrições do global mudaram.
Fase 1 adiciona frescor e ordenações: `declaredAt`/`estimatedAt` no select do
agregado de pledges (`lastPledgeAt`), `lastSignalAt` no view model via
`resolveMunicipalityLastSignalAt` (mesmo helper para o comparador do servidor e
a célula), e as sort keys `deficit` (novo default do staff) e `frescor`,
derivadas em memória no cenário central.
Fase 2 dobra os sinais nas células existentes: "há N dias" na coluna de última
atualização (frio ≥ 21 dias, em âmbar — vermelho já é da prioridade), badge
"sem responsável" em prioridade alta sem assessor, coluna da vergonha no
detalhe da métrica de assessoria, tooltip de cobertura nomeando o cenário e
copy Praças→Municípios.
Cortes: votos em jogo → B13, LQ/captura → E10, coluna dedicada de déficit
desnecessária.
Nova rota staff-only /campanha/conceitos (glossário curado em
src/lib/campaignIntelligenceConcepts.ts, revisado junto das fórmulas
que descreve em municipalityPotential.ts/goalCoverage.ts) com
descoberta por "Saiba mais" nos tooltips do card "Conta da cadeira" e
entrada fixa no rodapé da sidebar (staffSecondaryNav). Corrige defeito
E8 de formatElectionNumber (meta fracionária lida como cem mil em
pt-BR). Ajustes pós-/simplify: MetricExplanation.conceptID obrigatório,
href da sidebar desacoplado do módulo de conteúdo (client bundle).
Meta Bom/Regular/Mínimo duplicated the estimate scenarios — keep a single municipality series (pessimista/média/otimista), backfill then drop the old columns, and map the spreadsheet onto expectedVotes.
- knip (files/deps at error level; unused exports ledgered as warn) with
config for Payload/Next entries; deleted the dead modules it confirmed:
RecentlyVisited + UpcomingActionPlansCard + actionPlanUpcomingPreview,
lazy-dialog helpers, tseZone, federalCandidateTotalsAggregate (test-only),
unused ui kit files, one-shot rename script; pruned unused radix deps and
typescript-eslint; added pg/@types/pg (pinned to drizzle's 8.10.2)
- GitHub Actions CI on the mirror: lint, typecheck, knip, unit, migrate, int
against a Postgres 17 service
- eslint: unused-vars with ^_ ignore patterns (migrations args exempt),
repo-wide ban on `as never` (no-restricted-syntax), lint gate at
--max-warnings=0; fixed all 54 pre-existing warnings (dead imports,
hooks deps, tracking-pixel img disable with impeccable config exception)
- tsconfig: include consolidated from 219 lines to 5 (wildcard covers all
.next dist variants), noUnusedLocals on
- typed replacements for every as-never: hookFilledCreateData (actions
create data completed by collection hooks), drizzle scopeColumn narrowing,
tests' stub<T> helper (property-level checking kept, completeness waived)
- test infra: leased-consent generalization used by onda0 spec (shared
leases over the stable consent keys; window race ledgered), purge fix
- docs: IMPROVE-CODE-QUALITY-PLAN.md tracker, TECH-DEBT.md ledger,
TESTING.md safety-net map (package.json also gains the Phase 2
build:election-aggregates script entry; the script lands next commit)
Replace the `Function`-typed payload param (no-unsafe-function-type lint error
blocking pre-push) with `Pick<Payload, 'find' | 'create'>`, which also surfaced
missing Lexical `version` fields in the consent fixture — now added.
Rename the operational unit "Praça"/"Núcleo" to "Município" (code identifier
`municipality`) and land the 2026-07-23 course changes:
- New `candidate` role with unrestricted visibility (`isCampaignStaff` now
includes it) alongside coordinator/advisor; leader is locked down to the
supporter contact tool only.
- Demandas become staff-only; dobradinhas modeled via a `stateDeputy` entity.
- Camaçari consolidated to a single município (ZE 170/171 aggregated);
Salvador keeps its 19 zone municípios.
- Map moves to the staff dashboard; `/campanha/municipios` is list/overview.
- Vote pledges keep the declared vs estimated (scenario) asymmetry; leaders
never see estimates.
Destructive migration for the campaign vertical (drops nucleus/plaza-era
tables, wipes campaign rows, seeds 435 municípios). Public-site data untouched.
Includes the post-delivery /simplify cleanups: shared reloadStaffActor /
reloadCoordinatorActor helpers, req.context cache for getFreshCampaignUser,
parallelized dashboard loads, candidate dashboard-copy fix, and dead
leader-dashboard / roleKind code removal.
436 seeded Praças (municipality-based; per-TSE-zone in Salvador/Camaçari) become
the operational unit; roles renamed to coordinator/advisor/leader with advisors
scoped to administered Praças. Leadership is one record per person across many
Praças/organizations; vote pledges split leader-declared vs staff-estimated
(estimates never serialized to leaders). Adds organizations, demand workflow
with staff-only cost/receipts, action-plan results with deputy presence, manual
political trend, multi-candidate comparison and the diverging red/white/blue
map mode. Migration 20260721_020109_remodel_plazas deliberately resets the
campaign vertical in production (public site untouched); test suite ported to
the new model (unit 187 / int 306 / e2e 9 green).
Align the campaign desk with quieter KPI strips, queues-first dashboard, shared shells, and advanced filters; capture Impeccable design context and the remaining simplify/critique debts on the roadmap.
Phase 1 UX: StrictCombobox municipality in SupporterForm, AlertDialog in
RemoveSupporterDataButton. Phase 2 DRY: campaignListUrl + CampaignListPagination
+ campaignFormFields + mapCampaignFormActionError shared across nuclei,
supporters and action plans. Phase 3: single SQL aggregate for supporter list
KPIs via drizzle in the Payload txn. Phase 4: bulk drizzle insert for CSV import
inside the existing Payload txn with ON CONFLICT DO NOTHING, plus a fail-closed
skipContactPhoneInvariant context flag. Phase 5: HMAC-signed single-use import
token backed by a new supporterImportBatch staging collection so the full ok-row
set never crosses the Server Action boundary twice.
Replace scalar region/city/neighborhood with hasMany arrays so nuclei can span multiple Bahia municipalities while keeping neighborhoods scoped to a single city.
Adds electoral nuclei, leaderships, updates, and WhatsApp invite flows behind campaign auth, plus the consolidated schema migration that Vercel applies on deploy.