diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index 8d171fa3..b31635e9 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -7,6 +7,7 @@ alwaysApply: false ## Status +- **B41 entregue em código (2026-07-27):** scroll horizontal + coluna Município sticky em `/campanha/municipios` (`md+`). Espelha **B21 ✓** `TerritoryList`: `containerClassName="overflow-x-auto"`, coluna `name` com `sticky left-0 z-20`/`z-[5]` + `min-w-56 bg-background`; wrapper `overflow-visible` mantido. Cards mobile inalterados (**B42**). Sem migration/action/Consent. Critique: P1 `min-w`×`max-w` corrigido; edge fade Adiado. Gate tsc/lint/format/knip(P3)/cycles/540 unit/413 int/build; Aikido 0 findings. Plano: [`scroll-horizontal-lista-municipios.md`](../../../docs/plans/scroll-horizontal-lista-municipios.md). - **C8 F4b entregue em código (2026-07-27):** o guard `campaign formActions convention` em `codebaseConventions.unit.spec.ts` só via `…/formActions.ts`, então os irmãos (`lifecycleFormActions`, `resultFormActions`, `updateFormActions` ×2, `pledgeFormActions`, `municipalityStaffFormActions`, `taskActions`) ficavam sem verificação. Filtro ampliado para basename `/^\w*[Ff]ormActions\.ts$/` + `taskActions.ts`; 6 ladders migradas para `runCampaignFormAction`/`runCampaignRedirectFormAction`; `taskActions.ts` allowlisted (contrato posicional para `useOptimistic`). Docstring do wrapper precisada: exceção de atividades = só `atividades/formActions.ts`. Sem migration/UI. Gate: tsc/lint/format/knip (P3 pré-existente)/cycles/unit 540/build; int relacionado (activity + votePledge) verde — suíte int cheia flocou com `too many clients` no Postgres compartilhado. Aikido Opengrep exit 2 pré-existente, 0 findings nos arquivos editados. Plano: [`escala-dry-pos-c6.md`](../../../docs/plans/escala-dry-pos-c6.md). - **B8+ entregue em código (2026-07-27):** os quatro débitos do `/simplify` do B8 F2, e o achado central mudou de dono. **F1 — a alavanca não era a que o plano dizia.** O plano tratava "escala de chão" como algo implementado via dígitos de quantização; medindo o re-snap do artefato commitado para grades mais grossas, quantização sozinha quase não corta: ~12 m → 8.990 pontos (99% do original), ~25 m → 8.164, ~50 m → 6.219, e **mesmo na grade da malha estadual (~103 m) → 3.849 pontos, ainda 203/feature** — ~9× o irmão. Quem manda é o **limiar absoluto de Visvalingam**, que o plano citava entre parênteses. Então a política nasceu invertida: o peso absoluto **em m² de chão** é a política, os dígitos são derivados do bbox de cada topologia. `scripts/lib/topology.mjs` (**primeiro módulo de `scripts/lib/` — o B5 F2 pousa nele, não abre um segundo**) expõe `SIMPLIFY_TOLERANCE_M2` por malha, `GROUND_GRID_METRES = 102.82`, `simplifyToGroundScale`, `quantizeToGroundGrid` e `describeGroundScale`; `SIMPLIFY_QUANTILE`/`QUANTIZE_DIGITS` morreram nos dois scripts. Resultado medido: as zonas caíram de **9.120 → 574 pontos de arco (480 → 30,2/feature) e 72.310 → 7.687 bytes**; `bahia-municipalities` saiu **byte-idêntico** (a política foi semeada no valor histórico dele); territórios mudou **1 byte** (a grade passou de 102,730 para 102,822 m ao ser derivada do bbox em vez de um `1e4` fixo). **Três fatos para ninguém re-derivar:** (1) tolerância é **por malha de propósito** — a municipal (1,78 km²) chega a 17,9 pontos/feature nas zonas, mas come **5% da área de terra de Salvador (306 → 291 km²)**, que é justamente a grandeza com que o int prova que nenhum bairro foi descartado nem duplicado; as zonas ficaram em 0,06 km² e os territórios em 13,6 km² (dissolve da malha municipal, já vêm generalizados); (2) **a ordem importa** — quantizar antes de simplificar colapsa vértices em duplicatas e o `merge` posterior produz anéis quebrados (medido: Salvador dissolvendo para 24 km² em vez de 307); (3) a área das zonas pós-corte é **306,3 km²** (era ~324), dentro da janela de 280–360 km² que o int já tinha. `MAX_TOPO_BYTES` ficou e ganhou ao lado **`MAX_POINTS_PER_FEATURE = 60`** — bytes só pegam malha que ninguém simplificou (as zonas viviam a 10% do teto de bytes com 22× a densidade do irmão — 26× é a razão do passo de grade, não da densidade). `zoneNumber` saiu do artefato e do tipo. **F2:** `featureMapKey` passou a rodar sobre features **reais** das três malhas, cada uma no laço que já a varre inteira — 417 `codarea`, 27 `code`, 19 `slug` (a zona responde slug mesmo carregando o `ibgeCode` da cidade); a primeira escrita asseria a cidade dentro do laço das zonas, e o `/simplify` mostrou que isso era **uma** feature municipal asserida 19 vezes (as 19 ZE compartilham o código de Salvador) enquanto 416 municípios e 27 TIs nunca passavam por `featureMapKey`; o caso de precedência do unit usava `{ municipalitySlug, codarea }`, forma que **nenhuma malha produz** — caiu, e o comentário passou a declarar o invariante real: as três propriedades de chave são **mutuamente exclusivas**, e `ibgeCode` na malha de zonas deliberadamente não é chave. **F3:** `.catch()` na metade das zonas — e a precisão que o plano deixava otimista: como o bundle é keyed por slug, **Salvador não volta a pintar agregada**, volta como polígono sem dado e sem interação (não está em `scopedKeys`); a memoização da rejeição fica (é o estado de falha acionável do B14) e `status === 'error'` segue reservado à malha municipal. A ressalva de aproximação saiu do gate `!comparisonActive` e virou `
` condicionado a `bundle.hasZoneMunicipalities` — campo novo do contrato, derivado de `kind === 'zona'` no loader. O `/simplify` rejeitou as duas alternativas: um `isZoneMapKey` no cliente (segunda codificação da regra de `mapKeyForMunicipality`, em espaço negativo) e `zoneBreakdown.length > 0` (recria o acoplamento que o fill-in da lista de zonas vai deletar); com o campo próprio, o fill-in só apaga o `
` e `BahiaMeshFeature`. **Um item do plano foi rejeitado por medição:** `city`/`zoneNumber` de `MunicipalityZoneNeighborhoodEntry` **ficam** — nenhum runtime os lê, mas os dois entram nas linhas hasheadas por `canonicalEvidenceRows` e são colunas do fixture de proveniência da RA 02/2017 (transcrição **independente** da resolução), então deletar enfraqueceria a verificação por ganho zero; só o `zoneNumber` do **artefato** saiu. Sem migration, collection, action ou `Consent`; e2e segue 436 paths / 435 interativos. Gate completo rodado bare contra o banco local (as falhas de e2e encontradas foram reproduzidas em árvore limpa — flakiness pré-existente já no ledger); Aikido **0 findings** nos arquivos editados (desta vez o scanner rodou; o exit code 2 do B8 F2 não se repetiu para o SAST). Plano as-built: [`escala-dry-pos-b8f2.md`](../../../docs/plans/escala-dry-pos-b8f2.md).
- **`users.roles` entregue em código (2026-07-27):** campo `roles` (`admin` | `editor`) em `users` — migration `20260727_032523_add_users_roles` (enum + tabela `users_roles` + backfill `admin` nas rows existentes). `isPayloadAdmin` exige papel `admin` (falha fechado se ausente/vazio); `hasPayloadPanelAccess` / `canManagePublishedContent` liberam editor em `/admin` + `post`/`tag`/`media`; `petition`/PII/campanha seguem admin-only; `canReadMunicipality` nega ator `users` sem admin (antes devolvia `Where` vazio e o editor via Municípios no sidebar). Unificação `users`+`campaignUser` **rejeitada** nesta sessão (25 FKs, isolamento estrutural→dado, janela de congelamento) — gatilho: Fase 2 white-label ou dor real de duas contas. Plano: [`roles-admin-payload.md`](../../../docs/plans/roles-admin-payload.md).
@@ -14,7 +15,7 @@ alwaysApply: false
- **B39 entregue em código (2026-07-27):** login de `/campanha` ganhou “Lembrar de mim neste dispositivo”, default desmarcado. Contrato: **8 h** padrão / **14 dias** opt-in, definidos em `campaignSessionTtl.ts`; `CampaignUser.auth.tokenExpiration` usa o teto longo porque Payload não aceita TTL por login e `useSessions` poda pelo `expiresAt` armazenado; `setCampaignAuthCookie` re-assina por `jwtSign` os claims do token para 8 h quando necessário e mantém JWT/`maxAge` em lockstep. Convite e reset continuam no default curto. “Sair” autentica o token e delega a revogação da sessão corrente à operação transacional pública `logoutOperation` do Payload antes de apagar cookie/PWA; falha de DB nunca impede a limpeza local. Sem migration (`migrate:create __probe`: “No schema changes detected”). Impeccable critique/polish: copy outcome-first, descrição ligada por `aria-describedby`, alvo medido **44×44 px**, sem overflow em 390/768/1440; detector 0. E2E mede `campaign-token.expires` em ~14 dias.
- **B32 entregue em código (2026-07-26):** editar Status de apoio na célula da lista de lideranças — `LeadershipListSupportStatusControl` (Popover + `NativeSelect`, trigger = `SupportStatusBadge` atual) **sempre montado** na coluna "Status" de `/campanha/liderancas`, sem gate de "modo de edição" (anti-goal nomeado, mesmo já rejeitado em B19/B28/B31 para esta tabela). Auto-save por debounce ~150 ms via `POST /campanha/liderancas/support-status` (novo route, espelho de `political-trend/`/`advisors/`), reusando `updateLeadershipInternal({ id, supportStatus })` sem `revalidatePath`. **Auditoria do plano antes de codar:** `isSameOriginRequest` e o shell `campaignJsonMutationRoute.ts` já existiam (extraídos por B24/B27) — este item só importa, não extrai; a extração pendente era só `leadershipStaffEditSafeMessages` (espelho de `municipalityStaffEditMessages.ts`), consumida pelo route novo e por `[id]/formActions.ts`. Terceiro call site de `supportStatusLabels` (já usado em `LeadershipInternalForm`/`LeadershipForm`) motivou extrair `src/utilities/leadershipLabels.ts`, que passou a ser fonte única também para `SupportStatusBadge` depois do `/simplify`. Verificação por e2e (não unit do route, molde inexistente para `political-trend`/`advisors`/`expected-votes` também) — `tests/e2e/campaignLeaderships.e2e.spec.ts`, primeiro spec da rota, com `Promise.all([waitForResponse, selectOption])` para não confirmar persistência antes do POST assentar. **`/simplify`:** consolidou os dois cadastros de label pt-BR (badge + `leadershipLabels`), colapsou `displayStatus`/`draft` (sempre idênticos) num único estado, trocou o parse manual pelo guard `isSupportStatus` já existente, e completou a extração de `leadershipStaffEditSafeMessages` nos dois irmãos que ainda inlinavam a mesma string (`liderancas/formActions.ts`, `liderancas/nova/formActions.ts`) — o inline em `actions/leadership.ts` ficou (mesmo precedente de `actions/municipality.ts`), e a decisão de coagir `null`→`a_abordar` (já travada no plano) não mudou. Sem migration/collection/Consent. Gate: tsc/lint/format/knip (erro pré-existente em `payload.config.ts`)/check:cycles/unit/int/e2e (suíte cheia com 1 worker; 2 falhas de `campaignMunicipalities.e2e.spec.ts` não relacionadas — B24/B27, `strict mode violation`/timeout por contenção de outra sessão concorrente no mesmo Postgres local — reproduzidas verdes isoladamente). Plano: [`autosave-status-lista-liderancas.md`](../../../docs/plans/autosave-status-lista-liderancas.md).
- **B8 F2 entregue em código (2026-07-26):** as 19 ZE de Salvador ganharam polígono próprio e o coroplético deixou de ser keyed por `codarea`. Antes, Salvador era **um** polígono carregando os votos da cidade inteira, então o mapa não respondia "qual pedaço de Salvador está fraco / é meu" — a única visão por zona era a lista textual do `zoneBreakdown`. **Fase 0 (prep S1+S3):** `municipalityZoneNeighborhoods` passou a autorar só `{ municipalitySlug, source, neighborhoods }` e hidrata `city`/`zoneNumber` de `getMunicipalityCatalogEntry` no `entry()` (slug desconhecido ou não-`zona` falha no import); int novo de ordem de slugs vs catálogo. S2 (gerar TS do JSON) segue cortado — a curadoria ficou no script, não no catálogo. **Fase 1–2 (artefato):** `scripts/build-municipality-zone-geometries.mjs` (`pnpm build:municipality-zone-geometries`, `--report` só reconcilia) baixa/cacheia `BA_bairros_CD2022.zip` do geoftp do IBGE, parseia com **`shpjs`** (devDependency nova — reprojeta o SIRGAS 2000, o IBGE não publica GeoJSON de bairro), filtra `CD_MUN === '2927408'` (**170 polígonos**) e dissolve por zona pela RA 02/2017 do TRE-BA → `src/lib/geometries/bahia-municipality-zones.topo.json` (**72.310 bytes**, ~20 KB gzip, 19 features keyed por `municipalitySlug`), com submódulo `bahiaMunicipalityZoneGeometries.ts` + `loadMunicipalityZoneGeometryModule()` no padrão memoizado de B5 F1. **Reconciliação = 3 tabelas curadas no cabeçalho do script, não regex mais esperta:** `IBGE_TO_TRE_NEIGHBORHOOD` (7 aliases — compostos "Beiru/Tancredo Neves", CAB, ilhas), `TRE_NEIGHBORHOODS_MERGED_INTO` (Jardim Cruzeiro dentro de Vila Ruy Barbosa; o build **recusa** um merge que atravesse zonas) e `SPLIT_NEIGHBORHOOD_ZONES` (Periperi, que a resolução corta na Rua das Pedrinhas, vai **inteiro** para a ZE 17 — o bairro é o átomo geométrico). Atribuição em dois passes: nome da resolução, depois **maior fronteira compartilhada** para os 5 bairros que a RA nunca nomeia (Ilha Amarela, Dois de Julho, Chame-Chame, Horto Florestal, Mirantes de Periperi) — a regra foi **validada** contra os 4 que a resolução nomeia sob outro rótulo e reproduz a zona oficial nos 4. O build morre se sobrar polígono órfão ou bairro TRE sem polígono (hoje 170/170 → 19/19). **Fase 3 (re-keying):** map key = `kind === 'zona' ? slug : ibgeCode` (`mapKeyForMunicipality` + `buildMunicipalitiesByMapKey` em `municipalityMapNavigation.ts`); o bundle inteiro re-keyed (`territorialClassByMapKey`, `projectedValidVotesByMapKey`, `municipalitiesByMapKey`, `valuesByYear`, `values2026ByScenario`, `validVotesByYear`, `diffByYear`); os rollups pararam de acumular com `+=` porque cada map key agora é 1:1; `computeAggregateTerritorialClass` deu lugar a `computeMunicipalityTerritorialClass` (um slug por chave); `resolveMunicipalityMapNavigation` virou 1:1 e o ramo `kind: 'zones'` morreu junto com o scroll-anchor e a copy "toque de novo" do readout. **`competitiveRank` continua por cidade** (o artefato TSE só tem `federalRankByIbgeCode`): as 19 zonas dividem a posição da cidade, o que a legenda já afirmava — int novo pina que as 19 têm **uma** posição só. Per-ZE segue fora (gatilho: produto pedir posição dentro da zona). `buildMunicipalitiesByIbgeCode` sobrevive intacto — o href de lista do B14 ✓ é por cidade. **Fase 4 (mapa):** `BahiaMap` monta a camada de município a partir de **duas** malhas via `loadLayerFeatures`, e o polígono municipal de Salvador fica desenhado **por baixo** como base sem dado e sem pointer (`baseKeys`), para que um sliver de desacordo entre duas malhas simplificadas independentemente não abra buraco no contorno do estado; `keyPropertyForMode`/`featureKeyFromProperties` deram lugar a `featureMapKey` (`municipalitySlug ?? codarea ?? code` — as três nunca coexistem, então o argumento de modo sumiu); `fitMapToHighlights` passou a medir os **paths desenhados** (`pathByKeyRef`) em vez do módulo de geometria, que já não sabe sozinho de qual malha veio a chave, e `geometryModuleRef` foi apagado. Copy do painel: a linha morta "Zonas não têm polígono oficial — Salvador e **Camaçari** aparecem agregadas" (Camaçari saiu do modelo de zona na M1) virou o disclaimer de aproximação. **Dois fatos medidos, para ninguém re-derivar:** o polígono municipal de Salvador tem **~690 km²** e a soma das 19 zonas **~324 km²** — a diferença é a água da baía que os limites municipais encerram e a malha de bairros não desenha, então o int de área compara contra a área de **terra** (280–360 km²) e não contra o município; e centróide-dentro-de-outra-zona **não** é teste de sobreposição válido aqui, porque a ZE 16 é concava e envolve a ZE 14 (o centróide da 16 cai na 14) — a garantia de não-sobreposição é estrutural (cada bairro é atribuído a exatamente uma zona) mais a soma de áreas. Testes: int de contrato (19 features, slugs == catálogo, budget 600 KB, centróide de cada zona dentro do polígono municipal), unit de `featureMapKey`, e2e `campaignZoneMap.e2e.spec.ts` (436 paths / 435 interativos — o único não-interativo é a base de Salvador — readout "Salvador — ZE N" e clique abrindo `/campanha/municipios/salvador-ze-N`). **`/simplify`:** comentário de `NearbyMunicipalityResolution.zoneCity` que dizia "sem polígonos de zona (B8 F2)" corrigido (os polígonos passaram a existir nesta entrega; o card **deliberadamente** continua na lista filtrada — débito aberto, não efeito colateral); duas asserções do int de bairros que viraram tautologia com a hidratação da Fase 0 (comparavam `city`/`zoneNumber` com o catálogo de onde foram copiados) removidas, apontando para o teste de fixture que checa contra a transcrição independente; e o script parou de re-indexar geometrias simplificadas por **nome** de bairro (o irmão `build-bahia-geometries.mjs` usa `codarea`, único por construção) — hoje são 170 nomes distintos em 170 polígonos, mas um nome repetido em malha futura colapsaria dois polígonos silenciosamente num artefato commitado; passou a haver `id` próprio, e o rebuild saiu **byte-idêntico** (sha256 `1dd0d629…`). Sem migration, sem collection, sem action, sem Consent. **Gate:** tsc/lint/format/knip/`check:cycles`/unit+int/e2e/build rodados; **Aikido não fechou** — o scanner MCP responde Opengrep exit code 2 em **qualquer** entrada (inclusive um arquivo-probe trivial de uma linha), então os "4 findings" que ele reportou são artefato da execução quebrada e não achados atribuíveis; o feed do Aikido não tem nada deste repo. SCA do `shpjs` também pendente. **Relatórios do `/simplify` chegaram depois do merge** e o que sobrou virou o fill-in **B8+** ([`escala-dry-pos-b8f2.md`](../../../docs/plans/escala-dry-pos-b8f2.md)), aberto por uma medição que contradiz a intuição de "malha pequena, artefato pequeno": as 19 zonas têm **9.120** pontos de arco contra **9.139** da malha de **todos** os 417 municípios da Bahia (480 vs 22 pontos/feature; Salvador é um `Polygon` de **35** pontos na malha municipal). A causa não é a malha de bairros ser detalhada — é que `QUANTIZE_DIGITS = 1e4` e `SIMPLIFY_QUANTILE = 0.35` foram copiados verbatim do script irmão e **os dois são relativos à própria topologia**: a quantização é uma grade sobre o bbox do arquivo, e o de Salvador é ~26× mais estreito, o que dá **~3,9 m** de resolução contra ~103 m no estado, num mapa que para em `maxZoom: 10`. Consequência para quem for consertar: o que deve ficar compartilhado entre os dois scripts é a **escala de chão**, não a contagem de dígitos — extrair as constantes como estão (o que o revisor de reuse pediu, sem essa medição) cimentaria o defeito e lhe daria autoridade de política. Os outros três: nada roda `featureMapKey` sobre um feature **real** fora do e2e, então as três escritas da map key podem divergir e pintar cinza sem erro em lugar nenhum; o `Promise.all` das duas malhas somado à memoização da rejeição fez de um chunk de 19 polígonos um ponto único de falha do mapa inteiro (antes a malha municipal sozinha bastava); e a ressalva "é aproximado, não é o limite oficial do TSE" está pendurada na condição de render da **lista** (`!comparisonActive`), então desaparece no modo comparação enquanto a malha aproximada continua pintada. Plano: [`poligonos-pracas-zona.md`](../../../docs/plans/poligonos-pracas-zona.md).
-- **B39–B42 registrados (2026-07-26, lote roadmap-items):** sessão "Lembrar de mim" (**B39**, ~0,5d, não cortável — default hoje 2 h); login biométrico WebAuthn (**B40**, soft B39, cortável); scroll horizontal + sticky coluna Município (**B41**, espelha TerritoryList/B21); polimento mobile lista municípios (**B42** — Drawer Tendência/Assessores no padrão B26 + card compacto). Planos: `lembrar-de-mim-sessao-campanha.md`, `login-biometrico-campanha.md`, `scroll-horizontal-lista-municipios.md`, `polimento-mobile-lista-municipios.md`. Dedup do lote: drawers + card mobile mesclados em B42; auth fatiado B39/B40.
+- **B39–B42 (lote 2026-07-26):** ~~**B39**~~ ✓ e ~~**B41**~~ ✓ entregues 2026-07-27; abertos: **B40** WebAuthn, **B42** polimento mobile. Planos: `lembrar-de-mim-sessao-campanha.md`, `login-biometrico-campanha.md`, `scroll-horizontal-lista-municipios.md`, `polimento-mobile-lista-municipios.md`.
- **E12 entregue em código (2026-07-26):** camada TI — rollups MAUP + benchmark intra-TI. Estende `territoryOverview.ts` (`computeTerritoryE12Rollup`: cobertura agregada E8 cenário `central`, captura regional como razão dos agregados, mediana/amplitude, município crítico e farol) e `loadTerritoryOverview.ts` (passa `user`, `loadMunicipalityGoalCoverageBundle`, `computeAggregateTerritorialClass` por TI e sub-linhas do Metropolitano). `/campanha/territorios`: colunas Cobertura/Captura/Classe, sorts e descrições B22 em `territoryListUrl.ts` / `territoryListLabels.ts` / `TerritoryListColumns.tsx`. Detalhe: `territoryIntraCaptureBenchmark.ts` + linha T4 no `MunicipalityGoalAccountCard`. Conceitos E18 `captura-regional` e `benchmark-intra-ti`. Cortes v1: sem agrupamento na lista de municípios, sem mapa TI. `sanityCheckSuggestedGoalsByTerritory` segue sem UI. Gate: tsc/lint/format/check:cycles/533 unit/406 int/build (com `PAYLOAD_SECRET` local); knip P3 pré-existente em `payload.config.ts`; Aikido 0 findings. Plano: [`camada-territorios-identidade.md`](../../../docs/plans/camada-territorios-identidade.md).
- **B36 entregue em código (2026-07-26):** coluna "Lideranças" em `/campanha/dobradinhas` — chips nomeados (link `/campanha/liderancas/[id]`) + Popover+Command + auto-save por delta, reusando **sem alteração** a mutação/schema/lock do **B31 ✓** (`setLeadershipStateDeputyMembership`, `leadership-state-deputies:{id}`, `nextStateDeputyIdsAfterMembership`). **Auditoria do plano** (escrito antes do B31 as-built) corrigiu duas defasagens: (1) sem `RelationOptionsProvider`/Context — B31 já tinha trocado Context por columns factory no `/simplify`; (2) casca `dobradinhas/formActions.ts` própria (não import cross-rota de `liderancas/formActions`). **Extração paga aqui:** `LeadershipStateDeputiesCell` (domínio) → `shared/LeadershipStateDeputyRelationCell` bidirecional (`direction: 'fromLeadership' | 'fromStateDeputy'`, `measureOverflow` default true — clamp 3 linhas + "Ver mais…" no lado do deputado; `liderancas` passa `false`). `loadLeadershipOptions`; `StateDeputyRowViewModel.leadershipCount` → `leaderships: {id,name}[]` na mesma query batch. Gate: tsc/lint/format/knip/cycles/unit 530 + int (flakes de timeout re-rodados verdes)/build; Aikido 0 findings. Plano: [`chips-liderancas-lista-dobradinhas.md`](../../../docs/plans/chips-liderancas-lista-dobradinhas.md).
- **B30 entregue em código (2026-07-26):** convite de completar cadastro por WhatsApp na lista `/campanha/liderancas` — `LeadershipInviteRowAction` (`UserPlusIcon`) na coluna `actions` existente (B28 ✓), ao lado do `MessageCircleIcon` de contato; Popover two-step (`Gerar convite` → Alert com âncora `wa.me` + copiar link) reusa `createCampaignInvite`/`kind: 'autopreenchimento'`; disable alinhado ao `whatsAppHrefForPhone` da mesma célula (`hasValidPhone`); sem migration/action/collection nova. Plano revisado pós-B28 (encaixe em `actions`, não coluna `inviteAction`). Gate: tsc/lint/format/cycles; 532 unit (+2) / 406 int; build não rodou sem `PAYLOAD_SECRET`; knip P3 pré-existente; Aikido 0 findings. Plano: [`convite-whatsapp-lista-liderancas.md`](../../../docs/plans/convite-whatsapp-lista-liderancas.md).
diff --git a/.impeccable/critique/2026-07-27T03-10-14Z__municipality-list-b41.md b/.impeccable/critique/2026-07-27T03-10-14Z__municipality-list-b41.md
new file mode 100644
index 00000000..faa863ea
--- /dev/null
+++ b/.impeccable/critique/2026-07-27T03-10-14Z__municipality-list-b41.md
@@ -0,0 +1,28 @@
+# Critique — MunicipalityList B41 (2026-07-27)
+
+Provenance: Assessment A + B via isolated subagents. Browser skipped (no local server). Detector exit 0, 0 findings.
+
+## AI slop
+
+No — Territory mirror with domain comment.
+
+## Strengths
+
+- Sticky Município keeps orientation while scrolling staff columns
+- Desktop/mobile split preserved (cards = B42)
+- Chrome matches Territory (overflow shell, z-index corner)
+
+## Issues addressed this session
+
+- **P1 fixed:** removed conflicting `max-w-52` with `min-w-56` on sticky name cell (aligned to Territory `min-w-56` only)
+- **P3 fixed:** E10 comment no longer claims the table cannot scroll horizontally
+
+## Deferred (plan rabbit holes / adiado)
+
+- **P1 edge fade:** plan Adiado — shadow/edge fade indicating more columns; revisit if field reports discoverability miss
+- **P2 row-hover tint on sticky cell:** Territory parent rows same solid `bg-background`; skip until 3rd sticky call site
+- **P2 denser sticky content (badge wrap):** accepted for v1; B17 column picker mitigates
+
+## Harden/optimize
+
+Out (no form/action/empty; no perf signal)
diff --git a/docs/plans/scroll-horizontal-lista-municipios.md b/docs/plans/scroll-horizontal-lista-municipios.md
index 19242853..04505dfa 100644
--- a/docs/plans/scroll-horizontal-lista-municipios.md
+++ b/docs/plans/scroll-horizontal-lista-municipios.md
@@ -1,12 +1,16 @@
# Scroll horizontal + coluna Município fixa na lista de municípios
-Status: rascunho
-Atualizado em: 2026-07-26
+Status: entregue
+Atualizado em: 2026-07-27
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B41)
Impeccable: B — encaixe em `CampaignTable` / `MunicipalityList` (desktop/tablet); sem rota nova
Appetite: ~0,5–0,75 dia eng; overflow-x + sticky left na 1ª coluna; reusa padrão de `TerritoryList`
Responsável: —
+**Revisão 2026-07-26 (auditoria pré-implementação):** questão aberta fechada — **A** (classes na column def `name` só; sem `stickyColumnId` no `CampaignTable` até o 3º call site). O wrapper desktop já usa `className="hidden overflow-visible md:block"` (sobrescreve o `overflow-hidden` do `CampaignTable`, como Territory) — **manter** ao trocar só `containerClassName` para `overflow-x-auto`.
+
+**Revisão 2026-07-27 (as-built):** `MunicipalityList` → `containerClassName="overflow-x-auto"`; coluna `name` com `sticky left-0 z-20` (head) / `z-[5]` (cell) + `min-w-56 bg-background` (espelho Territory; `max-w-52` removido no polish — conflitava com `min-w-56`). Mobile cards intactos. Critique: 0 P0; P1 width fix aplicado; edge fade permanece Adiado; sticky top+left layering igual Territory (header pode grudar no scroller interno do `overflow-x-auto` — aceitável). Gate: tsc/lint/format/knip(P3 pré-existente)/cycles/540 unit/413 int/build; Aikido 0 findings (Opengrep exit 2 conhecido).
+
## Design (Impeccable)
Âncoras: `PRODUCT.md` (Clarity under pressure; anti spreadsheet) / `DESIGN.md` · sistema de listas Pass 2 W1 · precedente **B21 ✓** (`TerritoryList`).
@@ -50,7 +54,7 @@ O precedente já existe na página de territórios (**B21 ✓**): [`TerritoryLis
## Questões em aberto
-- **Extrair helper de classes sticky para `CampaignTable`?** **Opções:** A) só classes nas column defs de municípios (e Territory já tem as suas) | B) prop `stickyColumnId` no `CampaignTable`. **Recomendação:** A no v1 (<3 call sites distintos de API); B quando B29/outras listas pedirem o mesmo seam. _(assumido.)_
+- **Extrair helper de classes sticky para `CampaignTable`?** **Resolvida (2026-07-26):** **A** — só classes nas column defs de municípios (Territory já tem as suas). **B** (`stickyColumnId` no `CampaignTable`) quando o 3º call site pedir o mesmo seam. _(assumido — validado na auditoria de implementação.)_
## Abordagem proposta
@@ -91,7 +95,8 @@ Depth check: copiar o padrão Territory, não um grid library.
## Adiado com gatilho
- **Sticky first column genérico no `CampaignTable`.** Revisitar no 3º call site (municípios + territórios + uma terceira).
-- **Sombra/edge fade indicando mais colunas à direita.** Revisitar se critique acusar descoberta fraca do scroll.
+- **Sombra/edge fade indicando mais colunas à direita.** Revisitar se critique acusar descoberta fraca do scroll (critique B41 2026-07-27 confirmou descoberta fraca — ainda Adiado até pedido de campo).
+- **Tint de hover na célula sticky.** Territory parent rows usam `bg-background` sólido; revisitar no 3º call site sticky (ou se a mesa reclamar da coluna “morta” sob hover).
## Referências
diff --git a/docs/roadmap.md b/docs/roadmap.md
index e5e6982b..dbb98b93 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -1,6 +1,6 @@
# Roadmap — Teqo
-Atualizado em: 2026-07-27 (**B32+** novo fill-in de engenharia registrado no fechamento do B32 — `MunicipalityListExpectedVotesControl`/`MunicipalityListTrendControl`/`LeadershipListSupportStatusControl` são o 3º call site da mesma máquina de auto-save por debounce (estado/refs/save/schedule/flush quase idênticos), gatilho que os três planos B24/B27/B32 já haviam registrado; auditoria primeiro, extração condicional (precedente B33+) — [plano](plans/escala-dry-pos-b32.md); **B32 ✓** editar Status de apoio na célula da lista de lideranças — `LeadershipListSupportStatusControl` (Popover + `NativeSelect`, sempre montado, sem toggle "Editar" de tabela inteira) grava por `POST /campanha/liderancas/support-status` (auto-save ~150 ms, sem `revalidatePath`), reusando `isSameOriginRequest`/`campaignJsonMutationRoute.ts` já extraídos pelo B24 ✓/B27 ✓; fecha o "Adiado com gatilho" do B29 — [plano](plans/autosave-status-lista-liderancas.md); **B33+ ✓** `useCampaignListFilterNavigation` — o scaffold de navegação/debounce sai dos três filtros de lista para um hook em `shared/` (281 linhas removidas contra 75 em `src`); a auditoria derrubou a premissa do plano de que os três domínios teriam assinaturas de canonicalização diferentes — `buildStateDeputyListHref`/`buildTerritoryListHref`/`buildMunicipalityListHref` **já canonicalizam internamente**, então o `parse(...)` explícito de dobradinhas/territórios era trabalho redundante e um único callback `toHref` cobre os três; a entrega achou **dois** bugs de comportamento, não de estrutura: tocar um filtro no meio da digitação **descartava** a busca pendente em dobradinhas e territórios (unificado na semântica de munícipios por `navigateWithSearch`), e o `setTimeout` do debounce capturava o `state` do render que o agendou, então um sort clicado dentro da janela de 1s era **revertido** um segundo depois — munícipios escapava por acidente, pelo `key=` da página que remonta e mata o timer (corrigido com `stateRef`, com caso de spec verificado vermelho nas três listas sem o fix); o `/simplify` ainda achou o fallback `useCampaignListPending() ?? useTransition()` copiado em 5 lugares, com o hook novo como 6º → `useCampaignListTransition`, e um `aria-live` que fazia o resumo de filtros de munícipios ser a **terceira** região viva da mesma página; `shouldUpdateMunicipalitySearchUrl` orfanou e foi deletado; `tests/unit/campaignListFilterNavigation.unit.spec.ts` (jsdom + router mockado + fake timers, 8 casos × 3 listas) é a primeira cobertura desses componentes — [plano](plans/escala-dry-pos-b33.md); **C8 F4b ✓** guard de `formActions` ampliado para irmãos `*FormActions.ts` + `taskActions.ts`; 6 ladders migradas; `taskActions.ts` allowlisted · [plano](plans/escala-dry-pos-c6.md); **Build resiliente ao global `metadata` ausente ✓** — `resolveSiteMetadata` em `src/utilities/seo.ts`; fallback `NEXT_PUBLIC_SITE_URL` e omit canonical/OG quando não há URL; 4 rotas + layout; [plano](plans/build-resiliente-metadata-ausente.md); **B8+ ✓** escala e DRY pós-B8 F2 — a política de escala de chão das malhas commitadas virou código nomeado (`scripts/lib/topology.mjs`, tolerância de Visvalingam em m² e dígitos de quantização derivados do bbox de cada topologia), e com ela a malha das ZE caiu de 480 para **30,2 pontos/feature** (72.310 → 7.687 bytes) sem que `bahia-municipalities` mudasse um byte; a auditoria corrigiu a alavanca do plano (quantização sozinha quase não corta — o peso absoluto manda) e **rejeitou** um item do F4 por medição (`city`/`zoneNumber` de `MunicipalityZoneNeighborhoodEntry` são evidência de proveniência hasheada, não código morto); `MAX_POINTS_PER_FEATURE = 60` é o novo tripwire, `featureMapKey` passou a ser exercido sobre features reais das três malhas (417 `codarea`, 27 `code`, 19 `slug`), o mapa **degrada** em vez de morrer quando o chunk das zonas falha, e a ressalva de aproximação saiu do gate de comparação · [plano](plans/escala-dry-pos-b8f2.md); **`users.roles` ✓** — `admin`\|`editor` em `users`, migration `20260727_032523_add_users_roles`, editor publica `post`/`tag`/`media`; unificação com `campaignUser` rejeitada · [plano](plans/roles-admin-payload.md); 2026-07-26 (**fill-in** remover lista "Municípios por zona eleitoral" do Início — pós-B8 F2 ✓ os polígonos tornam a lista textual redundante; dropar UI + `zoneBreakdown` do bundle; ressalva de aproximação fica na malha · [plano](plans/remover-lista-zonas-mapa-inicio.md); **B8 F2 ✓** polígonos das 19 ZE de Salvador — artefato `bahia-municipality-zones.topo.json` (72 KB) por dissolve bairro→zona do `BA_bairros_CD2022` do IBGE contra a RA 02/2017 do TRE-BA, com curadoria explícita e um passe de maior fronteira compartilhada validado; o coroplético passou a ser keyed por **map key** (`slug` na zona, `ibgeCode` no resto) e o polígono municipal de Salvador ficou como base sem interação por baixo, porque duas malhas simplificadas independentemente não fecham na mesma costa; `competitiveRank` segue por cidade (o artefato TSE só tem `federalRankByIbgeCode`) com int pinando o fato; desambiguar a ZE no card "Onde estou" do B14 ✓ virou débito com gatilho, não dependência — [plano](plans/poligonos-pracas-zona.md); **B8+** novo fill-in de engenharia com os quatro débitos do `/simplify` do B8 F2, cujos relatórios chegaram depois do merge — a malha de 19 zonas ficou com a mesma contagem de vértices da malha dos 417 municípios (as duas constantes de simplificação são relativas ao bbox e foram copiadas verbatim, o que vale ~3,9 m de grade em Salvador contra ~103 m no estado), a regra da map key está em três formas sem nenhum teste ligando artefato → `featureMapKey` → catálogo, uma falha de chunk nas zonas passou a derrubar o mapa inteiro, a ressalva de aproximação desaparece no modo comparação, e `getTerritoryFeature` perdeu o último consumidor de produção neste commit · [plano](plans/escala-dry-pos-b8f2.md); **B39–B42** registrados: sessão "Lembrar de mim" + login biométrico WebAuthn + scroll horizontal/sticky na lista de municípios + polimento mobile da mesma lista — [B39](plans/lembrar-de-mim-sessao-campanha.md), [B40](plans/login-biometrico-campanha.md), [B41](plans/scroll-horizontal-lista-municipios.md), [B42](plans/polimento-mobile-lista-municipios.md); **E12 ✓** camada TI — rollups MAUP, colunas Cobertura/Captura/Classe em `/campanha/territorios`, benchmark intra-TI no `MunicipalityGoalAccountCard`; [plano](plans/camada-territorios-identidade.md); **B36 ✓** coluna "Lideranças" em `/campanha/dobradinhas` — chips + Popover+Command + delta reuse da mutação B31; `LeadershipStateDeputyRelationCell` compartilhada (`direction`/`measureOverflow`); columns factory (não Context); casca `dobradinhas/formActions.ts`; `loadLeadershipOptions` + `leaderships[]` no row; [plano](plans/chips-liderancas-lista-dobradinhas.md); **B30 ✓** convite de completar cadastro por WhatsApp na lista de lideranças — `LeadershipInviteRowAction` (`UserPlusIcon`) na coluna `actions` ao lado do contato do B28 ✓; Popover two-step reusa `createCampaignInvite`/`autopreenchimento`; disable por `normalizeBrazilianPhone`; [plano](plans/convite-whatsapp-lista-liderancas.md); **B31 ✓** coluna de dobradinhas na lista de lideranças entregue — chips clicáveis → `/campanha/dobradinhas/
em tablet"]
B39n["B39 ✓ Lembrar de mim
/ sessão longa"]
B40n["B40 Login biométrico
WebAuthn"]
- B41n["B41 Scroll horizontal
+ sticky Município"]
+ B41n["B41 ✓ Scroll horizontal
+ sticky Município"]
B42n["B42 Polimento mobile
lista municípios"]
B15n["B15 ✓ Ordenar lista
por coluna"]
B16n["B16 ✓ Filtros no header
da lista"]
@@ -328,11 +328,11 @@ flowchart TD
B8F2 -.ZE Salvador = débito aberto.-> B14n
```
-Paralelizáveis agora: **B13** (E10 ✓ entregou as classes; consome `computeMunicipalityTerritorialClass` como está), **E12** (E8 ✓), **E11** (E9 ✓/E10 ✓ entregues — livre), ~~**B14**~~ ✓ (atalho geo no Início entregue 2026-07-26; ~~B8 F2~~ ✓ entregou os polígonos das ZE em 2026-07-26, então desambiguar Salvador virou débito com gatilho "pedido de campo", não dependência), ~~**B38**~~ ✓ (sidebar recolhido em tablet entregue 2026-07-26), **B39** (lembrar de mim / sessão longa; sem dep dura — cookie/JWT já existem; soft B40 só pela ordem no mesmo `LoginForm`), **B40** (login biométrico; soft B39 pelo TTL/copy — pode começar depois do B39), **B41** (scroll horizontal + sticky na lista de municípios; soft B16 ✓/B38 ✓ — não bloqueia), **B42** (polimento mobile da mesma lista; soft B26 ✓/B24 ✓/B27 ✓ — paralelo a B41), **B21** (página dos TIs; sem dep dura — E17 ✓/B15 ✓/B16 ✓ já entregues; E12 pousa nela depois; **B25** é o único item que espera B21 — o link do território na lista só existe depois do destino), **B17** (seletor de colunas; soft B15 ✓/B16 ✓ — pousa no header de coluna atual), **B18** (filtros salvos + submenu Municípios; soft B16 ✓/B17 — botão na barra), **B22** (explicação por coluna no header; sem dep dura — B15 ✓/B16 ✓/E18 ✓), **B23** (tooltip nas células; a mecânica genérica e a promoção do primitivo já saíram no E10 ✓ — restam os consumidores Assessores/Tendência e a convivência com o Popover), ~~**B24**~~ ✓ (auto-save + justificativa no popover de Tendência; entregue 2026-07-26), **B26** (registrar sinal na célula "Último sinal"; sem dep dura — E9 ✓/C12 ✓ já entregues; soft B24/B23 só pela coerência de contrato na mesma linha), **B27** (combobox + chips + auto-save no popover de Assessores; sem dep dura — B19 ✓ já deu o delta+lock e o padrão de chips; soft B24 pelo endpoint e B23 pelo gatilho), **B28** (e-mail + celular na lista de lideranças; sem dep dura — B19 ✓ é só o padrão soft de copy/WhatsApp; `phone` já está no view model), **B29** (sort + filtro no header da lista de lideranças; sem dep dura — B15 ✓/B16 ✓ deram o contrato e o popover; soft B21 e B28 só pela ordem de chegada: quem entrar depois herda o head compartilhado / encaixa as colunas do outro), fill-ins (renomear coluna Cobertura → quick win de densidade no header; remover coluna Tipo — quick win de densidade; remover lista de zonas do Início — pós-B8 F2 ✓; ícone de prioridade na lista; Cenário junto aos filtros — encaixa na barra slim de B16 ✓ —; selecionados no topo nos filtros do header — helper puro que B21 reusa —; O0+, RS+). **B30** (convite WhatsApp na lista de lideranças; sem dep dura — reusa integralmente a action/collection existentes; soft B28 só pelo ícone na mesma linha, qualquer um pode ir primeiro — **entregue 2026-07-26**), **B31** (coluna de dobradinhas na lista de lideranças; sem dep dura — o dado já está no view model e o catálogo já tem loader; soft B27 pela peça de célula, que só vira extração compartilhada no 3º call site, e B28/B29/B30 pela ordem de chegada na mesma tabela), ~~**B32**~~ ✓ (editar Status de apoio na célula da lista de lideranças; entregue 2026-07-26), **B33** (sort + filtro no header da lista de dobradinhas; sem dep dura — B15 ✓/B16 ✓ deram o contrato; soft B29/B21 só pela ordem de chegada na mesma corrida de extração do chrome compartilhado, quem entrar primeiro paga a peça e o outro só consome), **B34** (chips editáveis de municípios na lista de lideranças; sem dep dura — B19 ✓ já deu o delta+lock, o padrão de chips e o loader do catálogo a promover; soft B27/B31 pela mesma peça de célula editável — quem chegar depois herda o combobox/Popover em vez de reinventar —, e B28/B30/B32 só pela mesma tabela; _decisão a validar com produto_: Popover por célula em vez do toggle "Editar" pedido literalmente, mesmo precedente do B31/B32), **B37** (chips editáveis de municípios na lista de dobradinhas; sem dep dura — B19 ✓/B34 já dão a lib a promover e o desenho do Popover+combobox; soft B34 só pela ordem de chegada em quem faz a promoção da lib, B31 pela mesma família de pedido em outra tabela, B33 pela mesma tabela; _mesma decisão a validar com produto_ do B31/B32/B34: Popover por célula, não toggle). **B36** (coluna de lideranças na lista de dobradinhas; sem dep dura — reusa integralmente a mutação/schema/lock que o B31 desenha para a mesma relação, do lado oposto; soft B31 pela corrida de extração da célula/action compartilhadas, B19 ✓ pelo padrão de origem, B33 pela mesma tabela), **C13** (renomear Plano de Ação → Atividade; sem dep dura — não toca nenhuma das listas em curso (B26–B37 são municípios/lideranças/dobradinhas), então roda em paralelo a elas; a única ordem que importa é **antes de E13**, que constrói dentro da vertical renomeada, e antes do congelamento ~20/09 por carregar migration; PR mecânico e isolado, não misturar com feature). **D3** só após smoke + folga e só se o fluxo sede-digita (C12 ✓) não absorver os deltas do ZAP (não compete com E9).
+Paralelizáveis agora: **B13** (E10 ✓ entregou as classes; consome `computeMunicipalityTerritorialClass` como está), **E12** (E8 ✓), **E11** (E9 ✓/E10 ✓ entregues — livre), ~~**B14**~~ ✓ (atalho geo no Início entregue 2026-07-26; ~~B8 F2~~ ✓ entregou os polígonos das ZE em 2026-07-26, então desambiguar Salvador virou débito com gatilho "pedido de campo", não dependência), ~~**B38**~~ ✓ (sidebar recolhido em tablet entregue 2026-07-26), **B39** (lembrar de mim / sessão longa; sem dep dura — cookie/JWT já existem; soft B40 só pela ordem no mesmo `LoginForm`), **B40** (login biométrico; soft B39 pelo TTL/copy — pode começar depois do B39), ~~**B41**~~ ✓ (scroll horizontal + sticky — entregue 2026-07-27), **B42** (polimento mobile da mesma lista; soft B26 ✓/B24 ✓/B27 ✓ — paralelo a B41), **B21** (página dos TIs; sem dep dura — E17 ✓/B15 ✓/B16 ✓ já entregues; E12 pousa nela depois; **B25** é o único item que espera B21 — o link do território na lista só existe depois do destino), **B17** (seletor de colunas; soft B15 ✓/B16 ✓ — pousa no header de coluna atual), **B18** (filtros salvos + submenu Municípios; soft B16 ✓/B17 — botão na barra), **B22** (explicação por coluna no header; sem dep dura — B15 ✓/B16 ✓/E18 ✓), **B23** (tooltip nas células; a mecânica genérica e a promoção do primitivo já saíram no E10 ✓ — restam os consumidores Assessores/Tendência e a convivência com o Popover), ~~**B24**~~ ✓ (auto-save + justificativa no popover de Tendência; entregue 2026-07-26), **B26** (registrar sinal na célula "Último sinal"; sem dep dura — E9 ✓/C12 ✓ já entregues; soft B24/B23 só pela coerência de contrato na mesma linha), **B27** (combobox + chips + auto-save no popover de Assessores; sem dep dura — B19 ✓ já deu o delta+lock e o padrão de chips; soft B24 pelo endpoint e B23 pelo gatilho), **B28** (e-mail + celular na lista de lideranças; sem dep dura — B19 ✓ é só o padrão soft de copy/WhatsApp; `phone` já está no view model), **B29** (sort + filtro no header da lista de lideranças; sem dep dura — B15 ✓/B16 ✓ deram o contrato e o popover; soft B21 e B28 só pela ordem de chegada: quem entrar depois herda o head compartilhado / encaixa as colunas do outro), fill-ins (renomear coluna Cobertura → quick win de densidade no header; remover coluna Tipo — quick win de densidade; remover lista de zonas do Início — pós-B8 F2 ✓; ícone de prioridade na lista; Cenário junto aos filtros — encaixa na barra slim de B16 ✓ —; selecionados no topo nos filtros do header — helper puro que B21 reusa —; O0+, RS+). **B30** (convite WhatsApp na lista de lideranças; sem dep dura — reusa integralmente a action/collection existentes; soft B28 só pelo ícone na mesma linha, qualquer um pode ir primeiro — **entregue 2026-07-26**), **B31** (coluna de dobradinhas na lista de lideranças; sem dep dura — o dado já está no view model e o catálogo já tem loader; soft B27 pela peça de célula, que só vira extração compartilhada no 3º call site, e B28/B29/B30 pela ordem de chegada na mesma tabela), ~~**B32**~~ ✓ (editar Status de apoio na célula da lista de lideranças; entregue 2026-07-26), **B33** (sort + filtro no header da lista de dobradinhas; sem dep dura — B15 ✓/B16 ✓ deram o contrato; soft B29/B21 só pela ordem de chegada na mesma corrida de extração do chrome compartilhado, quem entrar primeiro paga a peça e o outro só consome), **B34** (chips editáveis de municípios na lista de lideranças; sem dep dura — B19 ✓ já deu o delta+lock, o padrão de chips e o loader do catálogo a promover; soft B27/B31 pela mesma peça de célula editável — quem chegar depois herda o combobox/Popover em vez de reinventar —, e B28/B30/B32 só pela mesma tabela; _decisão a validar com produto_: Popover por célula em vez do toggle "Editar" pedido literalmente, mesmo precedente do B31/B32), **B37** (chips editáveis de municípios na lista de dobradinhas; sem dep dura — B19 ✓/B34 já dão a lib a promover e o desenho do Popover+combobox; soft B34 só pela ordem de chegada em quem faz a promoção da lib, B31 pela mesma família de pedido em outra tabela, B33 pela mesma tabela; _mesma decisão a validar com produto_ do B31/B32/B34: Popover por célula, não toggle). **B36** (coluna de lideranças na lista de dobradinhas; sem dep dura — reusa integralmente a mutação/schema/lock que o B31 desenha para a mesma relação, do lado oposto; soft B31 pela corrida de extração da célula/action compartilhadas, B19 ✓ pelo padrão de origem, B33 pela mesma tabela), **C13** (renomear Plano de Ação → Atividade; sem dep dura — não toca nenhuma das listas em curso (B26–B37 são municípios/lideranças/dobradinhas), então roda em paralelo a elas; a única ordem que importa é **antes de E13**, que constrói dentro da vertical renomeada, e antes do congelamento ~20/09 por carregar migration; PR mecânico e isolado, não misturar com feature). **D3** só após smoke + folga e só se o fluxo sede-digita (C12 ✓) não absorver os deltas do ZAP (não compete com E9).
### Sequência por janela (só pendentes)
-**Janela 1 — agora → 05/08 (convenções):** smoke pós-deploy em produção + onboarding do time (Onda 0 §2/§4); **E4R ✓** em código (aplicar `pnpm db:seed:projecao` em produção após smoke); **E8 ✓** conta da cadeira entregue; **E17 ✓** tabela TI no Início; **B15 ✓** (sort por coluna) e **A11 ✓** (posição em votos + `sort=votos`; o default do staff passou a `deficit` no E9 ✓) entregues 2026-07-24; **E18 ✓** documentação de conceitos (`/campanha/conceitos`, v1 só E8) entregue 2026-07-24; **B19 ✓** gerenciar assessores (caminho do onboarding sem `/admin` — criar/ativar contas e carteiras) entregue 2026-07-24; **B16 ✓** filtros no header entregue 2026-07-25; **B20 ✓** Municípios prioritários no Início entregue 2026-07-25; ~~**B38**~~ ✓ sidebar recolhido em tablet (entregue 2026-07-26 — `offcanvas` + trigger `md+` + cookie/`matchMedia`); **B39** (lembrar de mim / sessão longa — o default de 2 h está derrubando sessão no onboarding; ~0,5 dia, entra cedo) e, logo atrás se folga, **B40** (biometria WebAuthn — cortável); **B41** (scroll horizontal + sticky na lista de municípios — tabela estoura tablet/desktop; ~0,5–0,75 dia) e **B42** (polimento mobile da mesma lista — Drawer em Tendência/Assessores + card compacto; ~1 dia, paralelo a B41); **B21** (página dos Territórios de Identidade — a leitura regional sai do painel do Início e ganha tela própria com sort/filtro; útil já no onboarding para dividir carteiras) se folga — e, logo atrás dela, **B25** (link do território na célula da lista: quarto de dia, é o que faz a página nova ser encontrada de dentro do trabalho); **B22** (explicação por coluna no header — a tabela deixa de exigir o autor do E8/E9 ao lado para ser lida; quick win de onboarding, meio dia); **B23** (tooltip nas células — nomes dos assessores no hover da coluna, hoje só iniciais, e a justificativa da tendência, hoje invisível na lista; capacidade genérica da célula, meio dia, casa com B22 pelo mesmo primitivo); ~~**B24**~~ ✓ (auto-save + justificativa no popover de Tendência — entregue 2026-07-26; a mesa corrige em massa as notas semeadas pelo E4R ✓ sem abrir 435 telas de edição); **B26** (registrar sinal na própria célula "Último sinal" — fecha o laço do E9 ✓: hoje a fila diz onde está frio e obriga a sair dela para registrar qualquer coisa; ~0,75 dia, e é o item que mais aumenta a chance do gate de adoção abaixo, porque o registro passa a caber no gesto de varrer a lista, inclusive no celular); **B27** (combobox + chips + auto-save nos Assessores — é literalmente a tela em que o CG divide as carteiras no onboarding: hoje procura o nome numa coluna de checkboxes e confirma num botão; ~0,75 dia, casa com B24 pelo mesmo endpoint e com B23 pelo mesmo gatilho); **B28** (e-mail + celular na lista de lideranças — copy + WhatsApp como em assessores, sem abrir a ficha; ~0,5–0,75 dia, quick win de campo no onboarding da rede); **B30** (convidar para completar cadastro por WhatsApp na mesma lista — reusa o convite que já existe no detalhe, sem abrir a ficha; ~0,25–0,5 dia, o quick win mais barato desta leva porque não toca action/collection nenhuma; casa com B28 pela mesma linha, ícone distinto); **B31** (coluna de dobradinhas na mesma lista — é o preenchimento em lote que o onboarding exige: o E4R ✓ deixou dobradinha só no município e as fichas vazias, e completar uma a uma pelo detalhe é o gesto que trava a mesa; ~1 dia, casa com B27 pela mesma peça de célula — quem for primeiro define o padrão do chip+combobox in-list); ~~**B32**~~ ✓ (editar Status de apoio na célula da mesma lista — entregue 2026-07-26; fecha o adiado do próprio B29); **B34** (chips editáveis de municípios na mesma lista — a coluna "Municípios" ganha o mesmo tratamento da coluna homônima em Assessores (B19 ✓): chips com link + Popover de edição com combobox, em vez do texto morto atual; ~1–1,25 dia, casa com B27/B31 pela mesma peça de célula editável — quem entrar antes define o padrão do combobox+chip in-list que os outros herdam); **B37** (o mesmo tratamento de chips editáveis na coluna "Municípios" de `/campanha/dobradinhas` — hoje um número morto, `municipalityCount`; ~0,75–1 dia, herda a lib promovida pelo B34 se ele já tiver landado, senão promove ela mesma); **B36** (a coluna "Lideranças" da mesma lista ganha o mesmo tratamento — chips nomeados + Popover + busca + auto-save por delta, reusando sem alteração a mutação que o B31 desenha para o lado oposto da mesma relação; ~0,5–1,5 dia dependendo de quem entre B31/B36 chega primeiro na corrida de extração da célula compartilhada); **B33** (sort + filtro no header de `/campanha/dobradinhas` — pedido explícito de reuso: Nome/Partido no header e filtro por Partido, sem duplicar o chrome de `/campanha/municipios`; ~0,5–1,5 dia dependendo de quem chega primeiro na corrida de extração do chrome compartilhado com B29/B21; entra perto de B29 por reusar a mesma peça); **B29** (sort + filtro no header da mesma lista — é a tela onde o assessor recebe a carteira e precisa recortar "quem ainda está a abordar aqui" / "quem ainda não tem acesso ao app" antes de convidar; ~1,5–2 dias porque paga também a extração do head compartilhado, então entra depois dos quick wins — mas antes se B21 for adiado, para o head nascer compartilhado); **B17** (seletor de colunas) / **B18** (filtros salvos + atalho sidebar) se folga de UX da lista no onboarding; fill-in **renomear coluna Cobertura** (header curto na fila — quarto de dia; glossário/KPI intactos); fill-in **remover coluna Tipo** (densidade morta na fila — quarto/meio dia; Salvador via slugs cobre ZE); fill-in **remover lista de zonas do Início** (densidade morta sob o mapa — quarto/meio dia; polígonos B8 F2 ✓ cobrem a navegação); fill-in **selecionados no topo nos filtros do header** (desmarcar sem caçar checkbox; helper puro que B21 herda — quarto de dia); **C13 ✓** (renomeado "Plano de Ação" → "Atividade" em toda a vertical, incluindo a migration data-preserving — entregue 2026-07-25); gate de adoção: sinal Little Hire — ≥1 update espontâneo até 30/07, cobrança da tabela dispensada (planilhas já em `docs/sheets/`) — acompanhamento em [IMPROVE-APP-PLAN.md](IMPROVE-APP-PLAN.md); **R6** critique/polish; **E9 ✓** entregue no mesmo dia; **B14 ✓** (atalho geo entregue 2026-07-26); Onda 0 jurídica em paralelo (externa).
+**Janela 1 — agora → 05/08 (convenções):** smoke pós-deploy em produção + onboarding do time (Onda 0 §2/§4); **E4R ✓** em código (aplicar `pnpm db:seed:projecao` em produção após smoke); **E8 ✓** conta da cadeira entregue; **E17 ✓** tabela TI no Início; **B15 ✓** (sort por coluna) e **A11 ✓** (posição em votos + `sort=votos`; o default do staff passou a `deficit` no E9 ✓) entregues 2026-07-24; **E18 ✓** documentação de conceitos (`/campanha/conceitos`, v1 só E8) entregue 2026-07-24; **B19 ✓** gerenciar assessores (caminho do onboarding sem `/admin` — criar/ativar contas e carteiras) entregue 2026-07-24; **B16 ✓** filtros no header entregue 2026-07-25; **B20 ✓** Municípios prioritários no Início entregue 2026-07-25; ~~**B38**~~ ✓ sidebar recolhido em tablet (entregue 2026-07-26 — `offcanvas` + trigger `md+` + cookie/`matchMedia`); **B39** (lembrar de mim / sessão longa — o default de 2 h está derrubando sessão no onboarding; ~0,5 dia, entra cedo) e, logo atrás se folga, **B40** (biometria WebAuthn — cortável); ~~**B41**~~ ✓ (scroll horizontal + sticky na lista de municípios — entregue 2026-07-27) e **B42** (polimento mobile da mesma lista — Drawer em Tendência/Assessores + card compacto; ~1 dia, paralelo a B41); **B21** (página dos Territórios de Identidade — a leitura regional sai do painel do Início e ganha tela própria com sort/filtro; útil já no onboarding para dividir carteiras) se folga — e, logo atrás dela, **B25** (link do território na célula da lista: quarto de dia, é o que faz a página nova ser encontrada de dentro do trabalho); **B22** (explicação por coluna no header — a tabela deixa de exigir o autor do E8/E9 ao lado para ser lida; quick win de onboarding, meio dia); **B23** (tooltip nas células — nomes dos assessores no hover da coluna, hoje só iniciais, e a justificativa da tendência, hoje invisível na lista; capacidade genérica da célula, meio dia, casa com B22 pelo mesmo primitivo); ~~**B24**~~ ✓ (auto-save + justificativa no popover de Tendência — entregue 2026-07-26; a mesa corrige em massa as notas semeadas pelo E4R ✓ sem abrir 435 telas de edição); **B26** (registrar sinal na própria célula "Último sinal" — fecha o laço do E9 ✓: hoje a fila diz onde está frio e obriga a sair dela para registrar qualquer coisa; ~0,75 dia, e é o item que mais aumenta a chance do gate de adoção abaixo, porque o registro passa a caber no gesto de varrer a lista, inclusive no celular); **B27** (combobox + chips + auto-save nos Assessores — é literalmente a tela em que o CG divide as carteiras no onboarding: hoje procura o nome numa coluna de checkboxes e confirma num botão; ~0,75 dia, casa com B24 pelo mesmo endpoint e com B23 pelo mesmo gatilho); **B28** (e-mail + celular na lista de lideranças — copy + WhatsApp como em assessores, sem abrir a ficha; ~0,5–0,75 dia, quick win de campo no onboarding da rede); **B30** (convidar para completar cadastro por WhatsApp na mesma lista — reusa o convite que já existe no detalhe, sem abrir a ficha; ~0,25–0,5 dia, o quick win mais barato desta leva porque não toca action/collection nenhuma; casa com B28 pela mesma linha, ícone distinto); **B31** (coluna de dobradinhas na mesma lista — é o preenchimento em lote que o onboarding exige: o E4R ✓ deixou dobradinha só no município e as fichas vazias, e completar uma a uma pelo detalhe é o gesto que trava a mesa; ~1 dia, casa com B27 pela mesma peça de célula — quem for primeiro define o padrão do chip+combobox in-list); ~~**B32**~~ ✓ (editar Status de apoio na célula da mesma lista — entregue 2026-07-26; fecha o adiado do próprio B29); **B34** (chips editáveis de municípios na mesma lista — a coluna "Municípios" ganha o mesmo tratamento da coluna homônima em Assessores (B19 ✓): chips com link + Popover de edição com combobox, em vez do texto morto atual; ~1–1,25 dia, casa com B27/B31 pela mesma peça de célula editável — quem entrar antes define o padrão do combobox+chip in-list que os outros herdam); **B37** (o mesmo tratamento de chips editáveis na coluna "Municípios" de `/campanha/dobradinhas` — hoje um número morto, `municipalityCount`; ~0,75–1 dia, herda a lib promovida pelo B34 se ele já tiver landado, senão promove ela mesma); **B36** (a coluna "Lideranças" da mesma lista ganha o mesmo tratamento — chips nomeados + Popover + busca + auto-save por delta, reusando sem alteração a mutação que o B31 desenha para o lado oposto da mesma relação; ~0,5–1,5 dia dependendo de quem entre B31/B36 chega primeiro na corrida de extração da célula compartilhada); **B33** (sort + filtro no header de `/campanha/dobradinhas` — pedido explícito de reuso: Nome/Partido no header e filtro por Partido, sem duplicar o chrome de `/campanha/municipios`; ~0,5–1,5 dia dependendo de quem chega primeiro na corrida de extração do chrome compartilhado com B29/B21; entra perto de B29 por reusar a mesma peça); **B29** (sort + filtro no header da mesma lista — é a tela onde o assessor recebe a carteira e precisa recortar "quem ainda está a abordar aqui" / "quem ainda não tem acesso ao app" antes de convidar; ~1,5–2 dias porque paga também a extração do head compartilhado, então entra depois dos quick wins — mas antes se B21 for adiado, para o head nascer compartilhado); **B17** (seletor de colunas) / **B18** (filtros salvos + atalho sidebar) se folga de UX da lista no onboarding; fill-in **renomear coluna Cobertura** (header curto na fila — quarto de dia; glossário/KPI intactos); fill-in **remover coluna Tipo** (densidade morta na fila — quarto/meio dia; Salvador via slugs cobre ZE); fill-in **remover lista de zonas do Início** (densidade morta sob o mapa — quarto/meio dia; polígonos B8 F2 ✓ cobrem a navegação); fill-in **selecionados no topo nos filtros do header** (desmarcar sem caçar checkbox; helper puro que B21 herda — quarto de dia); **C13 ✓** (renomeado "Plano de Ação" → "Atividade" em toda a vertical, incluindo a migration data-preserving — entregue 2026-07-25); gate de adoção: sinal Little Hire — ≥1 update espontâneo até 30/07, cobrança da tabela dispensada (planilhas já em `docs/sheets/`) — acompanhamento em [IMPROVE-APP-PLAN.md](IMPROVE-APP-PLAN.md); **R6** critique/polish; **E9 ✓** entregue no mesmo dia; **B14 ✓** (atalho geo entregue 2026-07-26); Onda 0 jurídica em paralelo (externa).
**Janela 2 — 05/08 → 16/08 (pré-propaganda):** C2 dados reais assim que o jurídico liberar; ~~**E9** fila de alocação~~ (entregue 2026-07-24, antecipada da janela 2); ~~**E16** dossiê do município~~ (entregue 2026-07-25, antecipado da janela 2); D2 se sobrar folga.
@@ -344,7 +344,7 @@ Paralelizáveis agora: **B13** (E10 ✓ entregou as classes; consome `computeMun
**Não cortáveis:** Onda 0 (jurídico/Consent); ~~**E4R** seed da planilha~~ (entregue 2026-07-24 — ainda aplicar em produção após smoke); ~~**E8** conta da cadeira~~ (entregue 2026-07-24); ~~**C12** registro-fundação~~ (entregue 2026-07-24); C2 dados reais; assimetria declarado×estimado (relação de campo); ~~**E9** fila de alocação~~ (entregue 2026-07-24 — era o mínimo de "inteligência, não planilha"); ~~**B19** gerenciar assessores~~ (entregue 2026-07-24 — CG/candidato não entram em `/admin`); **B39** lembrar de mim / sessão longa (default de 2 h derruba sessão no onboarding/campo — atrito diário de adoção).
-**Cortes seguros** (se o prazo apertar, nesta ordem): **D5** inbox→rascunhos (manter registro manual + `wa.me`); **D4** envio bridge (manter `wa.me`); **D3** fundação do canal (atalho `wa.me` continua); **E12** camada TI (rollup manual por lista; **E17** já dá a leitura regional básica no Início); **E13** planejador de giros (rebaixado na fila de corte em 2026-07-24: "perna"/agenda é a restrição dominante nomeada em campo — cortar só depois de E12; agenda segue manual com J-A/J-B como guia); **E15** backtest (pós-eleição por definição — cortar = perder o aprendizado 2030); **E11** motor v1 (manter a fila E9 ✓ sem sugestões); **B13** símbolo proporcional (manter quantis/LQ como escala); **E14** níveis (manter `priority` alta/normal); ~~**B8 F2** polígonos~~ (entregue 2026-07-26 — o mapa não é mais agregado em Salvador); **D2** push (adiar); **A6**; ~~**B14** município mais próximo~~ (entregue 2026-07-26); ~~**B38** sidebar recolhido em tablet~~ (entregue 2026-07-26); **B40** login biométrico (senha + B39 continuam — cortar custa só o atalho de digital/Face ID; o mais caro e o primeiro a cair desta leva B39–B42); **B42** polimento mobile da lista de municípios (Popover e card longos continuam usáveis — cortar custa conforto de teclado e densidade no celular); **B41** scroll horizontal + sticky (a tabela continua legível com zoom/pan da página — cortar custa o overflow em tablet; cai **depois** de B42 porque o bug de layout em mesa é mais caro que o polish mobile); **B18** filtros salvos (lista + filtros manuais continuam; Visitados cobrem “voltar”); **B21** página dos Territórios de Identidade (o painel do E17 ✓ no Início continua entregando a leitura regional) — **B25** cai junto por definição (sem destino, sem link); **B17** seletor de colunas (tabela completa continua; scroll horizontal); ~~**B22** explicação no header~~ (entregue 2026-07-26); ~~**B23** tooltip nas células~~ (entregue 2026-07-26); ~~**B24** auto-save + justificativa na Tendência~~ (entregue 2026-07-26); **B27** combobox + chips nos Assessores (a lista de checkboxes com "Salvar" continua atribuindo — cortar custa a mesma coerência do B24 mais o tempo do CG procurando nomes numa coluna durante o onboarding; corte junto com B24, já que dividem o endpoint); **B28** e-mail + celular na lista de lideranças (a ficha `/liderancas/[id]` continua mostrando o contato — cortar custa o gesto de copiar/abrir ZAP na varredura; quick win barato de cortar e de fazer); ~~**B30** convite de completar cadastro por WhatsApp na lista~~ (entregue 2026-07-26); **B31** coluna de dobradinhas nas lideranças (o `RelationMultiSelect` da ficha interna continua vinculando — cortar custa o preenchimento em lote do onboarding, uma ficha aberta por liderança, e mantém o vínculo invisível na varredura; corte junto com B27, já que dividem a peça de célula, e antes de B29 só se a mesa já tiver preenchido as dobradinhas à mão); ~~**B32** editar Status de apoio na célula~~ (entregue 2026-07-26); **B34** chips editáveis de municípios na lista de lideranças (o texto simples `row.municipalityNames.join(', ')` continua legível e a ficha `/liderancas/[id]` continua vinculando município via formulário — cortar custa o link direto ao município, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha; corte junto com B31, já que dividem a mesma peça de célula editável, e antes de B29 pelo mesmo motivo de coerência); **B37** chips editáveis de municípios na lista de dobradinhas (o número `municipalityCount` continua legível e a ficha de município continua vinculando dobradinha pelo `MunicipalityStrategyForm` — cortar custa o link direto, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha do município; corte junto com B34, já que dividem a mesma lib e a mesma decisão de container); ~~**B36** coluna de lideranças na lista de dobradinhas~~ (entregue 2026-07-26; a contagem `leadershipCount` era o fallback e a ficha `/liderancas/[id]` continua vinculando dobradinha via formulário — cortar custa o link direto à liderança, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha; corte junto com B31, já que dividem a mesma mutação/schema/lock); ~~**B33** sort + filtro no header das dobradinhas~~ (entregue 2026-07-26); **B29** sort + filtro no header das lideranças (a busca por nome continua funcionando e o escopo do assessor já reduz a lista — cortar custa o recorte por status/setor/município no onboarding da rede e mantém o header rico como cópia única de municípios; por ser o item mais caro desta faixa (~1,5–2 dias com a extração do head), cai **antes** do B26, que é o que sustenta o gate de adoção); **B26** registrar sinal na célula (o formulário completo em `/[slug]?tab=updates` continua sendo o caminho — cortar custa o laço da fila do E9 ✓ e o registro em campo pelo celular, que é justamente onde o gate de adoção se decide; por isso cai **depois** de B22/B23/B24); ~~**C13** renomear Plano de Ação → Atividade~~ (entregue 2026-07-25); **B20** prioritários no Início (lista `?priority=alta` continua; só perde o atalho — mas a seção atual mente, então cortar = conviver com o bug); fill-in **renomear coluna Cobertura** (a lista continua legível com o rótulo longo — corte = header mais largo); fill-in **remover coluna Tipo** (a lista continua completa; só mantém densidade morta — corte = conviver com a coluna); fill-in **remover lista de zonas do Início** (o mapa B8 F2 ✓ continua abrindo cada ZE — corte = conviver com 19 linhas redundantes sob o painel); ~~**B16** filtros no header~~ (entregue 2026-07-25); ~~**B15** sort por coluna~~ (entregue 2026-07-24); ~~**A11**/**E17**~~ (entregues 2026-07-24); ~~**E18** documentação de conceitos~~ (entregue 2026-07-24); ~~**E16** dossiê~~ (entregue 2026-07-25); demais débitos/fill-ins.
+**Cortes seguros** (se o prazo apertar, nesta ordem): **D5** inbox→rascunhos (manter registro manual + `wa.me`); **D4** envio bridge (manter `wa.me`); **D3** fundação do canal (atalho `wa.me` continua); **E12** camada TI (rollup manual por lista; **E17** já dá a leitura regional básica no Início); **E13** planejador de giros (rebaixado na fila de corte em 2026-07-24: "perna"/agenda é a restrição dominante nomeada em campo — cortar só depois de E12; agenda segue manual com J-A/J-B como guia); **E15** backtest (pós-eleição por definição — cortar = perder o aprendizado 2030); **E11** motor v1 (manter a fila E9 ✓ sem sugestões); **B13** símbolo proporcional (manter quantis/LQ como escala); **E14** níveis (manter `priority` alta/normal); ~~**B8 F2** polígonos~~ (entregue 2026-07-26 — o mapa não é mais agregado em Salvador); **D2** push (adiar); **A6**; ~~**B14** município mais próximo~~ (entregue 2026-07-26); ~~**B38** sidebar recolhido em tablet~~ (entregue 2026-07-26); **B40** login biométrico (senha + B39 continuam — cortar custa só o atalho de digital/Face ID; o mais caro e o primeiro a cair desta leva B39–B42); **B42** polimento mobile da lista de municípios (Popover e card longos continuam usáveis — cortar custa conforto de teclado e densidade no celular); ~~**B41** scroll horizontal + sticky~~ (entregue 2026-07-27) (a tabela continua legível com zoom/pan da página — cortar custa o overflow em tablet; cai **depois** de B42 porque o bug de layout em mesa é mais caro que o polish mobile); **B18** filtros salvos (lista + filtros manuais continuam; Visitados cobrem “voltar”); **B21** página dos Territórios de Identidade (o painel do E17 ✓ no Início continua entregando a leitura regional) — **B25** cai junto por definição (sem destino, sem link); **B17** seletor de colunas (tabela completa continua; scroll horizontal); ~~**B22** explicação no header~~ (entregue 2026-07-26); ~~**B23** tooltip nas células~~ (entregue 2026-07-26); ~~**B24** auto-save + justificativa na Tendência~~ (entregue 2026-07-26); **B27** combobox + chips nos Assessores (a lista de checkboxes com "Salvar" continua atribuindo — cortar custa a mesma coerência do B24 mais o tempo do CG procurando nomes numa coluna durante o onboarding; corte junto com B24, já que dividem o endpoint); **B28** e-mail + celular na lista de lideranças (a ficha `/liderancas/[id]` continua mostrando o contato — cortar custa o gesto de copiar/abrir ZAP na varredura; quick win barato de cortar e de fazer); ~~**B30** convite de completar cadastro por WhatsApp na lista~~ (entregue 2026-07-26); **B31** coluna de dobradinhas nas lideranças (o `RelationMultiSelect` da ficha interna continua vinculando — cortar custa o preenchimento em lote do onboarding, uma ficha aberta por liderança, e mantém o vínculo invisível na varredura; corte junto com B27, já que dividem a peça de célula, e antes de B29 só se a mesa já tiver preenchido as dobradinhas à mão); ~~**B32** editar Status de apoio na célula~~ (entregue 2026-07-26); **B34** chips editáveis de municípios na lista de lideranças (o texto simples `row.municipalityNames.join(', ')` continua legível e a ficha `/liderancas/[id]` continua vinculando município via formulário — cortar custa o link direto ao município, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha; corte junto com B31, já que dividem a mesma peça de célula editável, e antes de B29 pelo mesmo motivo de coerência); **B37** chips editáveis de municípios na lista de dobradinhas (o número `municipalityCount` continua legível e a ficha de município continua vinculando dobradinha pelo `MunicipalityStrategyForm` — cortar custa o link direto, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha do município; corte junto com B34, já que dividem a mesma lib e a mesma decisão de container); ~~**B36** coluna de lideranças na lista de dobradinhas~~ (entregue 2026-07-26; a contagem `leadershipCount` era o fallback e a ficha `/liderancas/[id]` continua vinculando dobradinha via formulário — cortar custa o link direto à liderança, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha; corte junto com B31, já que dividem a mesma mutação/schema/lock); ~~**B33** sort + filtro no header das dobradinhas~~ (entregue 2026-07-26); **B29** sort + filtro no header das lideranças (a busca por nome continua funcionando e o escopo do assessor já reduz a lista — cortar custa o recorte por status/setor/município no onboarding da rede e mantém o header rico como cópia única de municípios; por ser o item mais caro desta faixa (~1,5–2 dias com a extração do head), cai **antes** do B26, que é o que sustenta o gate de adoção); **B26** registrar sinal na célula (o formulário completo em `/[slug]?tab=updates` continua sendo o caminho — cortar custa o laço da fila do E9 ✓ e o registro em campo pelo celular, que é justamente onde o gate de adoção se decide; por isso cai **depois** de B22/B23/B24); ~~**C13** renomear Plano de Ação → Atividade~~ (entregue 2026-07-25); **B20** prioritários no Início (lista `?priority=alta` continua; só perde o atalho — mas a seção atual mente, então cortar = conviver com o bug); fill-in **renomear coluna Cobertura** (a lista continua legível com o rótulo longo — corte = header mais largo); fill-in **remover coluna Tipo** (a lista continua completa; só mantém densidade morta — corte = conviver com a coluna); fill-in **remover lista de zonas do Início** (o mapa B8 F2 ✓ continua abrindo cada ZE — corte = conviver com 19 linhas redundantes sob o painel); ~~**B16** filtros no header~~ (entregue 2026-07-25); ~~**B15** sort por coluna~~ (entregue 2026-07-24); ~~**A11**/**E17**~~ (entregues 2026-07-24); ~~**E18** documentação de conceitos~~ (entregue 2026-07-24); ~~**E16** dossiê~~ (entregue 2026-07-25); demais débitos/fill-ins.
## Bloqueadores atuais
diff --git a/src/components/campaign/municipality/MunicipalityList.tsx b/src/components/campaign/municipality/MunicipalityList.tsx
index 7a382b60..90112697 100644
--- a/src/components/campaign/municipality/MunicipalityList.tsx
+++ b/src/components/campaign/municipality/MunicipalityList.tsx
@@ -118,11 +118,11 @@ const VotePositionReadout = ({
/**
* E10 classe. In the table only the pill is visible — spelling the factors out
- * under it made this the widest cell in a grid that cannot scroll
- * horizontally, so the "por quê" moves to the column's `cellTooltip` (hover,
- * focus and tap) and stays as `sr-only` text; the card has the width to show
- * it outright. Either way the class never reaches anyone as a bare verdict,
- * which is the one thing it must never be.
+ * under it still bloats the sticky/dense row even with horizontal scroll,
+ * so the "por quê" moves to the column's `cellTooltip` (hover, focus and tap)
+ * and stays as `sr-only` text; the card has the width to show it outright.
+ * Either way the class never reaches anyone as a bare verdict, which is the
+ * one thing it must never be.
*/
const TerritorialClassReadout = ({
municipality,
@@ -262,11 +262,12 @@ const municipalityListColumns = ({
filterOptions={columnFilterOptions.name}
showPriorityFilter={isStaffView}
description={municipalityColumnDescriptions.name}
+ className="sticky left-0 z-20 min-w-56 bg-background"
>
Município
),
- cellClassName: 'max-w-52 whitespace-normal',
+ cellClassName: 'sticky left-0 z-[5] min-w-56 whitespace-normal bg-background',
cell: (municipality) => (
can't paint the row border, hence the
- inset shadow standing in for it. */}
+ {/* A sticky can't paint the row border, hence the inset shadow. */}