Commit graph

361 commits

Author SHA1 Message Date
Cursor Agent
673c5f3926
B148 — conta local na v2 (P/M/O + cobertura + classe)
- loadMunicipalityV2ContaData + MunicipalityV2LocalAccountSection
- Inline edit-in-place com cenário ativo local para cobertura
- Extract MunicipalityTerritorialClassRow do card E8
- Integra com B149 rede e B150 agora na mesma página v2
- Impl plan docs/plans/municipio-v2-conta-local-impl.md
2026-08-03 01:27:53 +00:00
Cursor Agent
9325380305
fix: resolve rebase conflict markers in v2 page 2026-08-03 01:03:19 +00:00
Cursor Agent
30a5a8a5e3
B151 — FAB secundário no detalhe município v2 (#334)
- Catálogo v2 de ações rápidas: dossiê, eleições, nova liderança, giro
- Integra no chassis B126 via registry + context sync na rota v2
- Omitido registrar sinal (já na faixa de status B147)

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-03 01:03:10 +00:00
Francisco Solla
2414001606
Merge pull request #343 from fsolla/cursor/pool-i333-5914f625-bd1e
B150 — Município v2 bloco Agora (encaminhamento + sugestão + visita)
2026-08-02 21:53:38 -03:00
Cursor Agent
24611e4e35
fix: resolve v2 page merge with B149 network section 2026-08-03 00:39:37 +00:00
Cursor Agent
33d2e5ba01
chore: format B145 detail pages 2026-08-03 00:29:48 +00:00
Cursor Agent
9bcbfaf96d
fix: normalize activity detail action row indentation (simplify)
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-03 00:29:48 +00:00
Cursor Agent
5696aef9ed
B145 — move entity identity to shell header on detail pages
- Detail routes return null from campaignPageChrome catalog (override per page)
- SetCampaignPageChrome + generateMetadata on 8 entity detail routes
- Remove body hero (h1, Voltar links, duplicate identity chips)
- Keep operational badges/chips in page body
- Update zone map e2e to assert shell chrome title slot

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-03 00:29:48 +00:00
Cursor Agent
debfd37919
style: prettier B150 files 2026-08-03 00:28:54 +00:00
Cursor Agent
6955cccc6b
B150 — Município v2 bloco Agora (encaminhamento + sugestão + visita)
- Loader e seção Agora na rota /campanha/municipio/[slug]/v2
- Encaminhamento editável com auto-save via POST /municipios/next-steps
- Até 2 sugestões E11 com resolve/dispensa existente; silêncio explícito
- Visita E13 condensada em uma linha + link ao compositor de giros
- Revalidação v2 ao resolver sugestão; helper compartilhado de empty state E11

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-03 00:28:47 +00:00
Francisco Solla
deefe566b0
Merge pull request #341 from fsolla/cursor/pool-i332-0d1ca390-b998
B149 — Município v2 — rede (lista edit-in-place)
2026-08-02 21:24:44 -03:00
Francisco Solla
fd29d0ecb4
Merge pull request #327 from fsolla/cursor/pool-i304-dfa7cbb3-3c18
B137 — Colunas ao lado da omnibox (e fim do “Ordenado por …” residual)
2026-08-02 21:21:28 -03:00
Cursor Agent
c95d88562e
format + e2e prewarm for pledge autosave routes 2026-08-03 00:11:22 +00:00
Cursor Agent
7d89356710
B149 — Município v2 rede: lista edit-in-place com top-N lideranças
- Seção Rede na rota v2 com tabela/lista densa e CTAs Ver todas / Nova
- Células autosave (B9) para declarado e estimado via JSON routes
- Loader + view puro com ordenação por estimado/declarado
- Revalidate v2 em writes de pledge; unit tests de merge/sort

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-03 00:10:30 +00:00
Cursor Agent
4c6cad9f41
B137 — format 2026-08-03 00:06:16 +00:00
Cursor Agent
e88073d892
B137 — simplify pass: dedupe picker type, derive territory columns
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-03 00:06:16 +00:00
Cursor Agent
d6455b1333
B137 — Colunas ao lado da omnibox e fim do Ordenado por residual
Move CampaignColumnPicker to omnibox trailing via CampaignColumnPickerTrailing
on all seven B17 list surfaces (+ organizações). Remove the picker row from
CampaignTable and drop visible sort-summary spans on territórios, lideranças
and dobradinhas (sr-only table captions keep sort context).

Closes #304

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-03 00:06:15 +00:00
Cursor Agent
a2553e1f03
merge main into B147; keep optional sinal body for v2 status
Align with B134 engagement write path; preserve B147 sinal empty-motivo
(collection + zod) required by the status-strip confirm rite.
2026-08-02 23:48:29 +00:00
Cursor Agent
6b8aa8b6cf
fix(campanha): B147 simplify — sinal empty body + cold select
Allow empty motivo on sinal through collection beforeValidate; distinguish
warm-typeless vs cold in the v2 signal select; reuse list signal form action.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 23:33:46 +00:00
Cursor Agent
0aa47f8673
feat(campanha): B147 município v2 shell + faixa de status
Rota paralela /campanha/municipio/<slug>/v2 com selects de nível, tendência e
sinal (modal de motivo opcional), classe territorial e agregado. Soft-dep B134:
motivo opcional no movimento de nível e remoção do campo “voltar atrás” do
write path/lista.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 23:29:05 +00:00
Francisco Solla
6539d0cc55
Merge pull request #329 from fsolla/cursor/pool-i288-97b0de1d-f857
B134 — Motivo opcional (tendência + nível); remover voltar atrás do DB
2026-08-02 20:25:23 -03:00
Francisco Solla
8a60b6f0f7
Merge pull request #326 from fsolla/cursor/pool-i310-73006baf-d8e9
B142 — Assessores — filtrar por município da carteira na omnibox
2026-08-02 20:22:16 -03:00
Cursor Agent
b931092320
B142 — format + impl plan deferred debts note 2026-08-02 23:09:01 +00:00
Cursor Agent
1841e722f7
B142 — simplify: remove dead filter exports, centralize hasActiveFilters
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 23:08:17 +00:00
Cursor Agent
6d358f71ba
B143 — formatação Prettier 2026-08-02 23:06:34 +00:00
Cursor Agent
7563fe45c3
B143 — usa Map para rótulos de dobradinha no facet
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 23:05:50 +00:00
Cursor Agent
4d4764893e
B142 — filtrar assessores por município da carteira na omnibox
Adiciona dimensão municipality na URL e omnibox da lista de assessores,
espelhando o precedente de lideranças: chips removíveis, busca tipada no
catálogo e filtro server OR via municipality.advisors.

Closes #310

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 23:05:33 +00:00
Cursor Agent
c9fdc59ed2
B143 — omnibox de lideranças filtra por organização e dobradinha
- URL state: organization / stateDeputy (OR inclusivo por dimensão)
- Facets cross-filtered em leadershipData
- Adapter omnibox + wiring na página
- Testes unit/int e plano de implementação

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 23:04:45 +00:00
Cursor Agent
5335f69b43
B134 — motivo opcional (tendência + nível); remove reversalSignals
- Make engagement level note optional in schema and UI
- Remove reversalSignals field from popover, action snapshot and DB
- Add data migration to strip reversalSignals from movimento snapshots
- Wizard trend note step no longer requires text; labels show (opcional)

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 23:02:53 +00:00
Cursor Agent
f98042e656
style: prettier format B133 files 2026-08-02 21:20:55 +00:00
Cursor Agent
f89fc4b81f
feat(campanha): B133 — listas sem subtítulo de prosa + filtro salvo no header
- Remove prose subtitles from list routes, Quadro and create/edit chrome
- Keep subtitles on Organizações, Conceitos, Perfil, Contatos (out of scope)
- MunicipalityListPageChrome: saved filter name as header subtitle when URL matches
- Edit pages: title-only chrome override
- Impl plan: docs/plans/listas-sem-subtitulo-prosa-impl.md

Closes #283

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 21:16:46 +00:00
Cursor Agent
0903576f9e
B128 — Adotar barra omnibox nas demais listas /campanha
Estende o chassis B127 (CampaignListOmnibox + adapters puros por domínio)
para territórios, lideranças, dobradinhas, apoiadores, atividades, demandas,
organizações e assessores. Remove pilhas antigas (search debounced, collapsible,
mobile selects, CampaignFilterChips). URL contracts extraídos para módulos
client-safe onde necessário (demand/organization/advisor). Testes unitários
nos adapters + hook navigate-only.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 20:55:12 +00:00
Cursor Agent
21e20a6719
feat(campanha): B127 omnibox filter bar chassis + Municípios pilot
Replace the stacked município filter controls with a shared omnibox
(chips + suggestions) backed by the frozen list URL contract; scenario
and non-default sort appear as presentation chips.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 20:30:55 +00:00
Cursor Agent
7676d7c5e1
B129 — Format municipios page after rebase conflict resolution 2026-08-02 20:03:58 +00:00
Cursor Agent
b4f2e2fa0a
B129 — Remove municipality list overview panel
Remove MunicipalityListOverview from /campanha/municipios and drop the
overview field/computation from loadMunicipalityListPageBundle. Filters,
scenario selector, and per-row goal coverage for sorting remain unchanged.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 20:03:58 +00:00
Francisco Solla
7839a88a5a
Merge pull request #270 from fsolla/cursor/pool-i250-d89b6314-599f
B123 — Campanha — orientação na shell + aba (sem títulos grandes de seção)
2026-08-02 16:16:58 -03:00
Cursor Agent
39f69e603b
fix(B123): pass notification bell as server slot in desktop header
Client component cannot import async CampaignNotificationBellSlot directly;
mirror CampaignMobileTopBar slot pattern to fix staff page hangs in e2e.
2026-08-02 18:48:30 +00:00
Cursor Agent
dbce18002c
B126 — FAB de ações rápidas substitui drawer persistente
Remove o bottom drawer não-modal (snap/peek/scroll-coupling) e introduz
um FAB em todas as larguras fora do Início e wizards. Abrir o FAB mostra
overlay modal: Dialog no desktop (busca em cima) e Drawer no mobile
(ações em cima), reutilizando strip e busca globais do Início.

Closes #260

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 18:39:27 +00:00
Cursor Agent
d4425251c6
style: prettier em arquivos B123 2026-08-02 18:11:54 +00:00
Cursor Agent
ccfbf592a8
B123 — orientação na shell + aba sem títulos de seção no corpo
Move page title/subtitle to CampaignMobileTopBar, CampaignDesktopHeader,
and tab metadata (Solla - Campanha - %s). Strip section headers and scope
chips from list/create routes; detail pages keep entity h1.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 18:08:47 +00:00
Cursor Agent
62b6001ed8
OPS14 — form-action guard sweeps ladder, not filename
Replace the W4d filename-based convention spec with a sweep of every
src/app/(campaign) file that calls mapCampaignFormActionError without
runCampaignFormAction/Redirect. Migrate profile, leaderSupporter, and
convite-login to the shared wrappers; allowlist auth/password and the
existing bespoke formActions exceptions with documented reasons.

Closes #241

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 16:47:42 +00:00
Cursor Agent
2e3740ef3f
Format B110 wizard return path changes 2026-08-01 20:40:04 +00:00
Cursor Agent
d5697bcfcb
B110 — wizard return path via allowlisted from query
Propagate return path from quick actions through the B98 wizard chain so
dismiss, skip, and chain end navigate back to the originating page instead
of always redirecting to Início.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 20:39:24 +00:00
Cursor Agent
f80f672732
B106 — format 2026-08-01 18:38:50 +00:00
Cursor Agent
84365ddf54
B106 — Voltar no header mobile, Android back e skeleton das sugestões
- CampaignHomeSearchChromeContext + publisher no Início
- CampaignMobileTopBar troca SidebarTrigger por Voltar quando uiFocused
- useHomeSearchFocusHistory com pushState/popstate para back do SO
- HomeSearchSuggestSkeleton enquanto isFetching sem hits
- Unit tests para history helper, top bar e skeleton

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 18:36:28 +00:00
Cursor Agent
653f04062b
B102 — fix drawer search crash on focus/type (TooltipProvider scope)
Lift TooltipProvider in the campaign app layout so it wraps
CampaignAppScrollChrome (scroll + quick-actions drawer). Focus→suggest
with priority-alta hits mounted CampaignHoverTooltip outside the old
provider and crashed the page. Unit pins cover focus→suggest and type→search.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 18:18:19 +00:00
Cursor Agent
d8f54a5d73
B98 — format Prettier nos arquivos do encadeamento 2026-08-01 11:42:08 +00:00
Cursor Agent
346949d035
B98 — Encadear ajustes do wizard após a ação principal
Após salvar (ou Pular) votos/tendência/sinal/liderança, navega ao
próximo elo da matriz v1 no mesmo município via entryAction, em vez
de voltar ao Início. Helper puro wizardActionChain + skips alinhados.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 11:41:26 +00:00
Cursor Agent
5ce82e2229
B97 — Salvar tendência fecha wizard (redirect home quando status já aplicado)
Após salvar a nota, o RSC refresh via server action via
redirecionava à etapa de escolha. Agora  manda
para Início nesse caso; o client usa  no sucesso.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 10:54:51 +00:00
Cursor Agent
20c69bd0de
B94 — wizard empty state: município mais próximo (geo)
No idle do passo de busca de município, resolve client-side o município
mais próximo no escopo do ator quando a permissão de geo já está granted
(reuso B14: campaignGeolocation + municipalityProximity + malha lazy).

- Prop RSC accessibleMunicipalities na rota acoes/[slug]
- Hook useWizardNearestMunicipality (fail-soft, só inScope)
- Merge geo no topo da lista B92 via wizardMunicipalitySuggestMerge
- Unit tests do merge/format

Closes #94

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 07:43:42 +00:00
Cursor Agent
dbf6a7fdce
B93 — wizard empty state continuity (visitados + última ação)
Merge client-side recent visits and last-acted municipality storage into
wizard municipality suggest idle list ahead of B92 server rankings.

- Add campaignLastActedMunicipality + wizardMunicipalitySuggestMerge libs
- Extend wizard-municipality-suggest with scopeMunicipalities for hit lookup
- Record last-acted on wizard write success (votes, signal, trend, leadership)
- Clear last-acted on logout alongside recent visits

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 07:21:56 +00:00
Francisco Solla
7f7c707d4d
Merge pull request #98 from fsolla/cursor/pool-i92-174dd43e-5e6f
B92 — Empty state do wizard: chassis + ranking de esquecidos
2026-08-01 04:17:53 -03:00
Francisco Solla
269fb79f37
Merge pull request #97 from fsolla/cursor/pool-i42-980ec2b6-2133
C11 — Escala planos (picker pós-C12 + paginação das demandas vinculadas)
2026-08-01 04:16:27 -03:00
Cursor Agent
4ed2787d36
Fix wizard suggest signal tie-break and route typing
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 07:03:20 +00:00
Cursor Agent
1ed65d0d81
B92 — wizard idle suggest: rank + loader + UI
Add wizard-municipality-suggest mode to home-search with cold-first
ranking (signal → E14 → trend → E10 class → 2022 votes). Wizard
municipality step now fetches suggestions when query is idle.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 07:01:19 +00:00
Cursor Agent
2ac6f4c8fa
style: prettier (C11 touched files) 2026-08-01 06:39:34 +00:00
Cursor Agent
8372cec91d
C11: picker remoto de atividades na demanda + paginação de vínculos
- Substitui loadActivityOptions (pagination: false) por AsyncSearchCombobox
  com busca server-backed filtrada por município (searchActivityRelationOptions).
- Pagina card Demandas vinculadas no detalhe da atividade (10/página) com
  soma de custo em query separada e link Ver todas na lista filtrada.
- Adiciona filtro ?activity= na lista /campanha/demandas.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 06:37:53 +00:00
Cursor Agent
ca80da178c
style: Prettier on B82 files 2026-08-01 06:24:54 +00:00
Cursor Agent
d5da7b0adf
B82 — Ações rápidas em lideranças (lista + detalhe)
Registra catálogo contextual no drawer B79 para /campanha/liderancas:
lista com update-leadership; detalhe com cinco wizards e prefill de
município quando N=1. Adiciona query leadershipId no wizard A4 e
CampaignQuickActionContextSync na ficha.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 06:24:54 +00:00
Francisco Solla
7261f3f588
Merge pull request #87 from fsolla/cursor/pool-i35-456a9692-831a
RS+ — Auth read leve + shells de senha
2026-08-01 03:23:51 -03:00
Cursor Agent
5f5b513336
style: prettier 2026-08-01 06:09:33 +00:00
Cursor Agent
104d6d4589
fix(RS+): depth:0 sem select parcial; form schema de convite separado
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 06:08:53 +00:00
Cursor Agent
7484aab28a
RS+ Fase 1+3: auth read leve, password fields e login state
- authenticateCampaignToken usa depth:0 + select mínimo (sem join media)
- getCampaignUserWithAvatar para layout/perfil; hasCampaignSession em rotas públicas
- CampaignPasswordFields reutilizado em reset, perfil e convite
- LoginForm migra para CampaignFormActionState + mapCampaignFormActionError
- invite login schema unifica refineMatchingPasswords

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 06:07:28 +00:00
Cursor Agent
f27563d694
style: Prettier (O0+ #34) 2026-08-01 06:04:44 +00:00
Cursor Agent
cbe171c597
O0+ (#34): consent key DRY, SQL int tests, Footer condicional, B68 suggest RSC
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 06:04:05 +00:00
Cursor Agent
be317191b6
B87 — Ações rápidas assessores (unrestricted)
Registry B79 para /campanha/assessores (lista + detalhe): catálogo
new-advisor com deep link ?criar=1; mount gate só unrestricted;
/novo redireciona para o deep link; busca global permanece na lista
(B91 no drawer).

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 05:48:08 +00:00
Cursor Agent
7812dee1f3
style: Prettier (B85) 2026-08-01 05:26:12 +00:00
Cursor Agent
c05b04236e
B85 — Ações rápidas no drawer de Demandas (lista + detalhe)
Registra catálogo contextual no chassis B79:
- Lista: um launcher register-demand → /demandas/nova (wizard A5 ainda stub)
- Detalhe: A1–A5 com prefill de município via context sync
- Expõe municipalityId no view model para prefill do form de criação

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 05:26:12 +00:00
Cursor Agent
b7ad9ad326
merge: sync with main (B80 municipalities quick actions) 2026-08-01 04:44:03 +00:00
Cursor Agent
eea2d9afa1
B84 — Ações rápidas na vertical Atividades (lista + detalhe)
Registra catálogo B84 no registry B79: Nova atividade e Planejar giro na
lista; wizards A1–A5 com ?municipio= no detalhe quando há município, mais
atalhos Editar/Tarefas/Atualizações. Context bridge no detalhe; drawer
omitido em /atividades/giros (compositor E13 já tem CTAs).

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 04:35:38 +00:00
Cursor Agent
6030d42e2f
B80 — Ações rápidas em Municípios (lista + detalhe A1–A5)
Registra catálogo staff no drawer B79 para /campanha/municipios e
/campanha/municipios/[slug]: lista espelha Início (sem prefill); detalhe
prefill ?municipio= nos wizards A1–A5 e omite Ver esquecidos.

Closes #16

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 04:21:15 +00:00
Cursor Agent
7a876ec334 refactor(notifications): bell loaders read through owner access, no bypass
countUnreadNotifications/loadNotificationList took a caller-supplied
recipientID with module-level admin bypass — one misuse away from reading
someone else's inbox. They now take the actor (CampaignUser) and read
with overrideAccess: false under canReadOwnNotifications, keeping the
explicit recipient filter as a second fence. Pin: owner isolation at the
loader level in campaignNotifications.int.spec.ts.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-07-31 20:57:18 -03:00
Cursor Agent
6f42bc3bf3 fix(miss-52): one shared Drawer for the chip-relation lists too
The B42 fix reached the municípios mobile cards, but the three
chip-relation surfaces still mounted a Drawer root per OPENED cell on
coarse pointers (lideranças 50 cells, dobradinhas 50, assessores 25):
the exact hundreds-of-hidden-instances class that tripped React #130 in
prod. Each now wraps its table in CampaignListSheetProvider — cell
bodies portal into the single host Drawer, fine-pointer popover path
untouched (behavior delta: after opening N cells sequentially on touch,
N drawer roots -> 1).
2026-07-31 13:17:19 -03:00
Francisco Solla
8dd60cf962 A6 — Dobradinha 2026 automática quando o TSE publicar candidaturas (pós-15/08) (#82)
* docs(A6): freshness audit da #27 no plano de dobradinhas 2026

electionInsights.ts foi deletado no Pass 2 W4a (modulo puro novo),
electionAlliances.ts nao nasce (campoParties + electionPartySpectrum ja
carregam a taxonomia), questoes em aberto resolvidas (tiers, score,
proporcionais, lockdown de lideranca), card na aba Eleicoes, overview da
lista fora de escopo, cache election-tse com bust na conciliacao Fase 5.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>

* feat(A6): insight de dobradinhas 2026 na aba Eleicoes do municipio

Lista ranqueada de candidatos proporcionais que concorrem de novo em 2026
(runningAgain2026=sim) por geografia do municipio: score 0,6 alinhamento
(campo 2026 + espectro Bolognesi) + 0,4 forca local normalizada pelo maximo
do conjunto. Estado pendente ate a conciliacao pos-15/08; Solla excluido
(referencia, nao parceiro); leitura derivada sem escrita; loader cacheado
sob a tag election-tse como os irmaos; link para /campanha/dobradinhas
como caminho de registro operacional.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>

* test(A6): cobertura unit/int do insight + lease das colecoes eleitorais

Unit: tiers (campo/espectro/falha-para-o-meio), score, ordenacao, empates,
limite, zero votos. Int: pending2026 sem conciliacao (mesmo com votos
locais), so adversarios, misto com exclusao de Solla/nao e geografia
alheia. Lease advisory election-collections serializa os dois specs que
tratam as colecoes eleitorais como test-owned (o import wipe-all apagaria
as fixtures do novo spec no meio do assert, 48-way paralelo).

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>

* refactor(A6): fixes do /simplify (3 reviewers paralelos)

P1: campo coalition morto removido do input/select/mapping (sem consumidor).
P1: hooks do lease com timeout explicito de 120s nos dois specs eleitorais
(o wait advisory passa dos 10s default do Vitest sob contencao — flake em
ambas as direcoes). P2: sonda de conciliacao via payload.count (idioma da
casa), desempate final por candidateNumber (ordem deterministica), geography
como prop na secao (sem lookup duplicado), casing Dep. Federal/Estadual
alinhado, marcador 1313 via BASELINE_TICKET_2022, entrada no CHANGELOG.

Declinados com justificativa: zones como tupla nao-vazia (churn em tipo
compartilhado por assert provada no catalogo) e early-start da promise
antes do Promise.all (loader-dentro-do-Suspense e o idioma da casa; ganho
so em cache frio).

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-07-31 00:14:27 -03:00
Cursor Agent
118544cbaa fix: remove leftover merge conflict markers in app layout 2026-07-31 02:37:17 +00:00
Cursor Agent
b756f5cdf4 style: prettier 2026-07-31 02:34:14 +00:00
Cursor Agent
a2eabaccba B79 — chassis do bottom drawer de ações rápidas + busca global (mobile)
Monta CampaignQuickActionsDrawer não-modal no layout (app) para staff fora
do Início e de /acoes, com snap collapsed/expanded, padding de peek no scroll,
registry vazio para catálogos B80+, e busca global reutilizada via
CampaignStaffGlobalSearch.

Closes #15

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-07-31 02:34:08 +00:00
Cursor Agent
de67acf41b D2 — Push + sino in-app para /campanha
- Collections Notification e PushSubscription com migration
- Sino no header (mobile + desktop) com drawer de avisos lidos/não lidos
- Hooks em municipalityUpdate, supporter, activity e convite aceito
- Push Web via web-push + handlers no service worker /campanha/sw.js
- Opt-in LGPD em /campanha/perfil (chave campanha-notificacoes-push)
- Testes unit/int de contrato e access

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-07-31 01:51:41 +00:00
Cursor Agent
90a97d3c76 style: prettier 2026-07-31 00:27:50 +00:00
Cursor Agent
6c8d7d418c B78 — Indicar carregamento na navegação do wizard
Reusa CampaignListPendingBoundary no layout autenticado para compartilhar
useTransition entre o header mobile e o conteúdo do passo. Voltar, X, Pular,
seleção de município e tiles de passo passam por CampaignWizardNavLink;
o corpo dima com aria-busy e o header mostra skeleton nos slots de título.

CampaignTransitionAnchor agora retorna a promise do router para manter o
pending honesto até a navegação terminar.

Closes #14
2026-07-31 00:24:39 +00:00
Cursor Agent
fcbc463990 fix: format + wire WIZARD_TREND_SAVED_MESSAGE for knip 2026-07-30 23:03:25 +00:00
Cursor Agent
f0f3d4534d B64 — Wizard mudar tendência (grid + justificativa + skip) 2026-07-30 23:03:25 +00:00
Cursor Agent
10377d90fb feat(campanha): remove mobile bottom nav (B73)
Remove CampaignBottomNav and getCampaignBottomNav — mobile navigation
is now sidebar-only (Sheet via SidebarTrigger). Drop the pb-24 padding
that reserved space for the fixed bar; scroll uses symmetric p-4 on mobile.

Impeccable: N/A (shell chrome removal, class B per plan).

Plan: docs/plans/remover-bottom-nav-mobile.md
2026-07-30 22:13:07 +00:00
Francisco Solla
142b0963e0 B54 — Busca global: layout responsivo dos resultados (#60)
* feat(campanha): responsive grid and viewport cap for home search results (B54)

Adds HomeSearchResultsLayout with 1/2/3-column breakpoints and allocateHitBudget so Municípios keep priority while total hits stay within per-viewport limits.

Closes #8

* fix(campanha): un-export knip-unused home search budget helpers
2026-07-30 18:45:44 -03:00
Francisco Ignácio Fontoura Solla
30170509f7 fix(e2e): WebAuthn on prod build, saved-filters navigation, Vercel scripts
CI e2e serves a production build on localhost; Next inlines NODE_ENV so
WebAuthn RP resolution now treats localhost requests as test mode. Saved
filters avoids a racing bare goto. Analytics/SpeedInsights render only on Vercel.
2026-07-30 17:37:20 -03:00
Francisco Ignácio Fontoura Solla
9eafe6081c feat(campanha): add Demandas group to home global search (B53)
Staff can find demands by title from the Início omnibox with municipality and status in the secondary line, scoped by campaignDemand access.
2026-07-30 12:39:35 -03:00
Francisco Ignácio Fontoura Solla
09fa21d4b3 B63: wizard registrar sinal (grid de tipos + passo de texto).
Two-step UX-1 flow on the B59 chassis with URL contract ?signalType= and optional ?entryAction= for embedded skip; reuses createMunicipalityListSignalFormAction.
2026-07-30 04:01:48 -03:00
Francisco Ignácio Fontoura Solla
29f6b32e91 refactor(campanha): simplify B70 wizard loader and tile a11y.
Lean tile loader avoids org/deputy fetches; server sorts names; info button is no longer nested inside the tile button.
2026-07-30 03:51:25 -03:00
Francisco Ignácio Fontoura Solla
dbd9d0fa3e feat(campanha): ship B70 leadership update wizard (grid + short form).
Replaces the post-municipality stub with a full wizard: tile grid, contact+leadership writes, entryAction skip, and Continuar after save.
2026-07-30 03:51:25 -03:00
Francisco Ignácio Fontoura Solla
625f25c984 B77+: simplify vote wizard helpers after single-screen delivery.
Unify violation highlights with getWizardVoteViolation, share the expected-votes save error constant, and drop dead parseWizardVoteDraft plus unused municipalitySlug prop.
2026-07-30 03:40:20 -03:00
Francisco Ignácio Fontoura Solla
f2bdb1ce3c B77: replace vote wizard ?cenario= ritual with single-screen editor.
Three scenarios on one step with confirm-only validation, legacy URL redirect, and updated tests/docs.
2026-07-30 03:31:57 -03:00
Francisco Ignácio Fontoura Solla
037785b280 B75: wizard mobile header chrome on Mandate Red bar.
Publish flow navigation from CampaignWizardShell via layout-level chrome so wizards no longer stack a second sticky header under the app bar.
2026-07-30 03:26:54 -03:00
Francisco Ignácio Fontoura Solla
37a7062428 B69: simplify the leadership model to exclusive + drop sector/consentNote.
Organization stays by product decision (political branching, e.g. SindMed);
the leadership gains `exclusive` (default true) with list badge and form
checkbox, and sector/sectorNotes/consentNote leave the collection, zod,
forms, list filters/sort, invite autofill and redemption.
2026-07-30 02:32:56 -03:00
Francisco Ignácio Fontoura Solla
34e5c6ed42 B61: wizard vote step with scenario passes and batch save.
Staff can adjust municipality expected votes through média→pessimista→otimista steps with shortcuts, coherence warnings, and a single POST on completion.
2026-07-30 01:16:19 -03:00
Francisco Ignácio Fontoura Solla
120ed8d9b5 B62: simplify the signal model to type + text (drop fonte/triangulado).
Drops signalSource/triangulated from municipalityUpdate (migration
simplify_municipality_signal_fields), rewrites signal-type labels for the
field team, and treats any adversary signal as confirmed in E11 (P1
triage 1). Also fixes the stale post-B43 leader-home e2e and lowers a
drifted bypass pin.
2026-07-29 23:24:00 -03:00
Francisco Ignácio Fontoura Solla
342e528c87 feat(E10+): Escala e DRY pós-E10
- F1: loadMunicipalityScopeFromDocs evita segunda query em municipalityPageData
- F2: computeAllMunicipalityPotentials memoizado por processo
- F3: TooltipProvider movido para o layout de (app)
- F4: DRY nos filtros multi do mobile em MunicipalityFilters
2026-07-29 23:18:25 -03:00
Francisco Ignácio Fontoura Solla
f6b73b3e63 B60: wizard municipality search step with shared staff loader.
Staff wizards pick a municipality via word-start search, auto-advance with ?municipio=, and reuse searchStaffMunicipalityHits extracted from B48.
2026-07-29 23:10:06 -03:00
Francisco Ignácio Fontoura Solla
67058d0027 B51: add Atividades group to staff home search.
Staff can find activities by title with scoped access, municipality+date secondary, and word-start matching on the existing home-search POST.
2026-07-29 22:57:02 -03:00
Francisco Ignácio Fontoura Solla
38459b7426 B52: add Dobradinhas group to staff home search.
Staff can find state deputies by name or party with municipality counts in the global Início search, mirroring the B49/B50 loader and UI pattern.
2026-07-29 22:52:35 -03:00
Francisco Ignácio Fontoura Solla
0109570b62 B65: anchor Início search and action strip to the mobile bottom.
The B46 flex spacer never filled the scrollport because min-h-full did not propagate; h-full min-h-0 plus home-dock groups the thumb zone above bottom nav.
2026-07-29 22:35:44 -03:00
Francisco Ignácio Fontoura Solla
2c80d9b54f B49: home search Lideranças group on staff Início.
Extends POST /campanha/home-search with leadership hits (word-start, advisor scope, cap 25) and HomeSearchLeadershipGroup below Municípios.
2026-07-29 22:14:30 -03:00
Francisco Ignácio Fontoura Solla
0e73d47a62 B50: home search Assessores group for unrestricted staff.
Word-start on name, municipality count on the row, shared HomeSearchHitRow; POST /campanha/home-search aggregates advisors alongside municipalities.
2026-07-29 22:09:42 -03:00
Francisco Ignácio Fontoura Solla
e733efbe42 B68: curated municipality suggestions on empty home search focus.
Staff see role-ranked shortcuts (advisor portfolio by signal age; CG/candidate by alta priority deficit) via POST mode suggest, without treating empty query as search.
2026-07-29 22:06:02 -03:00
Francisco Ignácio Fontoura Solla
dd23d2a44a B59: chassis do wizard de campanha (shell, rotas /acoes, hrefs no Início).
Staff ganha CampaignWizardShell, rotas /campanha/acoes/<slug> com passo ponte até B60, e strip do Início com links nos fluxos wizard.
2026-07-29 22:02:04 -03:00
Francisco Ignácio Fontoura Solla
49d5b6da3c B56: staff campaign summary on Início (central votes + E8 coverage). 2026-07-29 17:55:37 -03:00
Francisco Ignácio Fontoura Solla
e462d39fda B13+: concordância LQ, readout de escopo no mapa e DRY do build de agregados.
Rede de segurança para E15, copy honesta no Quadro para assessores, uma varredura federal por ano no artefato e tupla de rank no bundle RSC.
2026-07-29 16:26:43 -03:00
Francisco Ignácio Fontoura Solla
05072547cd B48: home search results for municipalities and identity territories.
Staff Início posts to /campanha/home-search with scoped word-start matches, shared readout component, and simplify hardening (messages, fetch busy, malformed response guard).
2026-07-29 16:05:56 -03:00
Francisco Ignácio Fontoura Solla
7bb7b5e261 B47: staff-only home search input with focused mode hiding action strip.
Debounced omnibox chassis and empty results region for B48+; no hit providers yet.
2026-07-29 15:31:46 -03:00
Francisco Ignácio Fontoura Solla
d04f23399f Fill-in: move estimate scenario into municipality list filters.
Cenário lives in MunicipalityFilters with provider wrapping filters and results; roadmap and plan marked delivered.
2026-07-29 15:27:46 -03:00
Francisco Ignácio Fontoura Solla
4658cdd686 B46: anchor home actions in thumb zone via CampaignHomeLayout.
Mobile flex spacer keeps the B44/B45 strip in the lower viewport above bottom nav; md+ keeps actions at the top and reserves searchSlot order for B47.
2026-07-29 15:06:45 -03:00
Francisco Ignácio Fontoura Solla
f9f9a14994 B45: mount persona home action catalog on /campanha.
Staff get six launchers with uncovered-municipalities linking to the sem_assessor list; leaders get two with contacts wired to the shared route constant.
2026-07-29 14:43:50 -03:00
Francisco Ignácio Fontoura Solla
926972fec6 B43: blank Início, Quadro staff home, and Contatos for leaders.
Split dashboard and leader contacts into dedicated routes, update nav and gates, and fix revalidation paths after the route move.
2026-07-29 14:02:35 -03:00
Francisco Ignácio Fontoura Solla
35a56ef3ee B17+: skip relation loaders when columns hidden and keep column picker on empty lists. 2026-07-29 07:53:07 -03:00
Francisco Ignácio Fontoura Solla
4806c417c8 Engineering Pass 3: consolidate patterns and lock them with guards.
Fold duplicated campanha access, loaders, list/filter/form shells, and scripts into shared modules; reorganize utilities by domain; stabilize e2e; and document prevention-first audit workflow so the same smells cannot creep back in.
2026-07-29 06:11:29 -03:00
Francisco Ignácio Fontoura Solla
77023db99d B34+: scale and DRY after municipality chip lists.
Memoize access reads per req, cache portfolio index, trim chip revalidates, and shrink RSC payload via catalog-backed labels.
2026-07-28 19:49:29 -03:00
Francisco Ignácio Fontoura Solla
be0d587005 Dim the tour composer proposal when changing identity territory.
Wire CampaignListPendingBoundary/Results on /campanha/atividades/giros so TI navigation matches list pending UX; pin contract in tourRegionPending unit spec.
2026-07-28 18:41:36 -03:00
Francisco Ignácio Fontoura Solla
8cae4adae9 E11 post-rebase simplify: clamp the composed rationale, extract firstFormActionMessage at its 3rd call site, keep metrics server-side. 2026-07-28 14:03:29 -03:00
Francisco Ignácio Fontoura Solla
189ad7a349 E11: suggestion engine v1 — the human-in-the-loop decision queue. 2026-07-28 14:03:29 -03:00
Francisco Ignácio Fontoura Solla
2cfb7d5e67 Simplify the WebAuthn adoption: an error branch that could not reach a screen.
`postJson` caught a non-JSON response and threw a plain `Error` carrying
"Resposta inválida do servidor.", but all three islands read `message` only off
a `CampaignWebAuthnError` — so the sentence was written for the person and then
thrown down the channel that discards messages. Deleted rather than re-typed:
the islands' own fallback is better copy for a field worker, which is the same
reason the five list-cell controls substitute their own. Nothing user-visible
changes, because the string was already unreachable; the comment now records
why the branch is absent, so it does not come back.

The login failure sentence was also spelled twice for one slot on one screen —
the route's `genericMessage` and the button's fallback — so it became
`CAMPAIGN_BIOMETRIC_LOGIN_ERROR_MESSAGE`, twin of the enroll constant the
register half already shares between its route and its card.

Also widened two doc comments this branch made stale: `campaignJsonMutationRoute`
and `campaignJsonRequest` both claimed to serve only the list cells' quick edits,
and four of nine callers are now WebAuthn ceremonies.
2026-07-28 12:30:36 -03:00
Francisco Ignácio Fontoura Solla
4d56a32ed2 Adopt B32+'s JSON route shell and fetch helper in the WebAuthn paths.
The rebase brought a convention guard that refuses a route.ts exporting POST
outside campaignJsonMutationRoute, and it named all four WebAuthn routes. They
now come from the shell, so the same-origin check is structural rather than
copied, and postJson delegates to postCampaignJson — keeping only what the
ceremonies add on top: the envelope's discriminant decides instead of the
status, and a non-JSON response reads as one sentence while a network failure
still propagates untouched. The two *-options routes have no body, hence
campaignWebAuthnNoBodySchema.
2026-07-28 11:33:22 -03:00
Francisco Ignácio Fontoura Solla
763f64080a Deliver B40: biometric login for /campanha.
WebAuthn platform passkeys: opt-in enrollment after a password login, a
password-less sign-in that mints the same 14-day campaign-token, and
revocation from the profile. The session is minted through Payload's public
pieces (checkLoginPermission, addSessionToUser, getFieldsToSign, jwtSign)
inside a transaction with an advisory lock, so a passkey login neither
bypasses a lockout nor evicts the account's other sessions. The rpID is
derived from NEXT_PUBLIC_SITE_URL and resolves to null when the request host
disagrees, so previews never offer a ceremony the browser would refuse.
2026-07-28 10:59:23 -03:00
Francisco Ignácio Fontoura Solla
c68e1d2394 Record the B37 closing triage on B34+ instead of a new ID.
The /simplify triage produced two live debts, and both are about the same pair
of functions the B34+ plan already names in F3, so they landed as F10 of that
plan rather than as an escala-dry-pos-b37: the município-side batch actions
write one município at a time (findByID + update each — ~870 round trips at the
435 ceiling B37 itself raised, but ~52 at the largest real TI, and no test above
a batch of 2), and the two are twins at 2 call sites, one short of the bar. The
common trigger is a "select all"/multi-TI affordance or a third batch on that
side, and F10 records why the bulk-SQL rewrite is not obviously the answer: it
would hand back the scope check that overrideAccess: false gives for free.

This delivery also closed B34+ phases outright, which is worth reading in the
plan rather than inferring from a diff. F2 is done and its prediction was wrong
in a way worth keeping: it stated "o B37 não retira este débito", and B37 read
the same evidence the other way — the ~165 measured lines were not deduplicated
into useChipRowOverflow, they were deleted when both cells became wrappers of
one engine. F9 is done via scopedPortfolioIndex. Half of F3 is done (the three
single-id deltas), and the half that stayed out — the set-with-floor form —
stayed out because seeing the collapsed code confirmed the plan's own inverted
recommendation. F5 and two F6 bullets shrank for the same reason: "written twice
across the two surfaces" stopped having two surfaces.

Also corrects a comment I got wrong in the previous commit: the batch's sort
does not carry the deadlock-avoidance guarantee, since acquireTextAdvisoryLocks
sorts its own keys (C8 batched them into one round trip); it only makes the
apply order stable.
2026-07-28 10:38:56 -03:00
Francisco Ignácio Fontoura Solla
0a63621d4b B37 simplify: one membership delta, cap messages named once.
nextStateDeputyIdsAfterMunicipalityMembership was the third byte-for-byte copy
of the same single-id delta — nextAdvisorIdsAfterMembership and
nextStateDeputyIdsAfterMembership differ from it only in which cap they read —
so the algorithm now lives once in nextIdsAfterMembership with the cap as data,
and the three named wrappers just bind their own. The three cloned spec files
became one describe.each suite: coverage went up rather than down, since every
relation now gets the "can go to zero" and no-mutation cases only B37's clone
had, plus an explicit assertion that each wrapper refuses at ITS cap with ITS
message.

That pairing matters because mapCampaignFormActionError matches thrown messages
against safeMessages by exact string, and two of those cap messages were bare
literals in three src places each (thrown, plus two independent allowlists):
rewording any one would have silently collapsed a real refusal into "tente
novamente". Both are constants now, next to their caps, following the
MUNICIPALITY_STATE_DEPUTIES_CAP_MESSAGE precedent already in those same files.
The dobradinha staff message had the same exposure in four places — and
actions/stateDeputy.ts already imported the constant while restating it as a
literal one line below — so it is STATE_DEPUTY_STAFF_MESSAGE (renamed: it
guards create/update too, not just the batch), and the conflict message it
shares with the create route is named too.

The batch action stopped resolving the dobradinha slug before its loop: the
slug exists only to target the revalidate, which the caller already skips when
nothing changed, so on a no-op batch that read was fetched and thrown away —
the guard setLeadershipMunicipalitiesMembershipRecord already had. Its
redundant second dedup is gone (the schema's transform already deduped; the
sort stays, for deadlock-free lock ordering), and the bare 435 shared with the
advisor batch is MAX_MUNICIPALITIES_PER_BATCH.

Two memoization gaps closed, both restoring invariants the codebase had already
established elsewhere. An advisor's scoped .filter() returned a fresh array per
row, defeating the WeakMap that exists precisely to share derivations across
rows, so scopedPortfolioIndex memoizes on (index, addableIds) and every row
gets one array. And LeadershipStateDeputyRelationCell called matchesAtWordStart
per candidate, paying an NFD pass plus three Unicode regexes on both sides per
keystroke over the unpaginated leadership catalog — the cost wordStartFilter's
own doc comment warns about; it now normalizes the query once against labels
cached on options, like municipalityPortfolio.

Rejected with reason: maxItems on the dobradinhas cell. A dobradinha has no
product ceiling on municípios (that is why the bound is the catalog), so a
"generous 30" would invent a rule that refuses a legitimate statewide
dobradinha, and the honest 435 is unreachable when ids are unique.

Also dropped the dead addPlaceholder knob no caller ever set.
2026-07-28 10:25:47 -03:00
Francisco Ignácio Fontoura Solla
da4ca2c713 Deliver B37: editable município chips on the dobradinhas list.
The "Municípios" column of /campanha/dobradinhas reuses
MunicipalityPortfolioCell unchanged, writing the inverted side of the
relation (municipality.stateDeputies) through a single transactional
batch action (setStateDeputyMunicipalitiesBatch): per-município advisory
lock, all-or-nothing on a partial-scope batch instead of a silent partial
apply, no floor (a dobradinha can hold zero municípios). stateDeputiesArraySchema's
cap moved 20 → 435 to fit a whole território batch from the município side.

The post-implementation critique found two P1s, and both were fixed instead
of deferred. First: MunicipalityPortfolioCell and LeadershipStateDeputyRelationCell
had grown the same chip-editing machine twice — optimistic delta, undo, ARIA
1.2 combobox, floor/ceiling, pointer-fine/coarse split — with incompatible
contracts. This was the 3rd call site B34+ was waiting for, so RelationChipCell
is now the single engine and both cells shrank to thin domain wrappers (chip/
search building, FormData, copy — nothing interaction-specific left in either).
LeadershipStateDeputyRelationCell also gained an ownerName prop and its first
dedicated test suite, which it shipped without before this.

Second: an advisor could see, and attempt to remove, a município chip outside
the portfolio they administer, failing silently at the document level.
MunicipalityPortfolioCell now filters the visible chips by addableIds instead
of only gating what can be added, so an advisor sees and can only touch
municípios they administer.

No migration, collection, or Consent. Docs updated: plan as-built, roadmap,
AGENTS.md, project notebook.
2026-07-28 09:34:39 -03:00
Francisco Ignácio Fontoura Solla
ed0fb4065f B17 simplify: inherit header labels, split persist from refresh.
The four *SortableHead islands already fall back to their sort-label record,
so B17's new `label` made the header text a third copy instead of removing the
second one — 16 redundant `children` deleted, leaving only `frescor`, which
diverges on purpose and now reads the record too.

The 3 s "safety net" timer was committing like a close, refresh included, which
recreated the per-toggle route render the 400 ms debounce was rejected for: at a
reading pace every checkbox landed outside the window. The timer now writes the
cookie only; closing repaints. A session that ends where it started refreshes
nothing, and a menu open when the route unmounts saves from the cleanup, since
Radix reports no close event for a sidebar navigation. Both pinned red first.

Also: `columnVisibility` required rather than optional for the tests' benefit,
the redundant per-list cap dropped, `resolveVisibleColumns` no longer copies,
and B29's unused `LeadershipListAccessFilter` export un-exported — knip was
failing on main too.
2026-07-28 09:17:46 -03:00
Francisco Ignácio Fontoura Solla
8bf872142b Give B29's leadership columns their B17 labels.
The rebase landed B29's sort/filter heads on /campanha/liderancas next to B17's
now-required `label`, and the two do not substitute for each other: B17 dropped
heads that only restated a label, but these carry the sort and filter controls,
so both sides survive on the same column. `updatedAt` is new from B29 and had
no label at all.

The four sortable ones read from `leadershipListSortLabels` rather than
repeating the string, which is the treatment B17 already gave municípios and
dobradinhas — the picker's label and the sort summary cannot drift now.
2026-07-28 08:27:19 -03:00
Francisco Ignácio Fontoura Solla
155f6492d9 B17: column picker on the seven CampaignTable list surfaces.
The picker shipped as a capability of CampaignTable rather than a municípios
feature, so every list that already uses the shared table got it: pass
`columnVisibility` and the table mounts the menu and filters the columns.

Persistence is a cookie, not localStorage, and the reason is a type rather
than a preference: CampaignTableColumn.cell is a function, so the column array
never crosses the server/client boundary and the client cannot rebuild the
table without it. The server filters, so a hidden column does not reach the RSC
payload either. `campaign_columns` carries a listId -> hidden ids map in a
compact form with no JSON and no percent-encoding, because document.cookie and
Next's cookies() disagree about decoding.

Closing the menu is the commit. The first cut debounced at 400 ms, which
batches a burst rather than a reading pace, so unchecking four columns while
reading their labels was four full-route refreshes — on /campanha/liderancas
each one re-pays the ~229 ms / 371-statement render already ledgered P1. The
timer survives at 3 s as a safety net for a menu left open; the price is that
the table behind the menu no longer updates per checkbox.

`defaultVisible` is gone: it was true on 10 columns and false on none, and
storing only the hidden set is what makes a newly shipped column visible to
everyone who never touched the picker. In exchange `label` is required, which
made `head` optional — 24 columns had a head that only restated the new label.
Each surface declares one mandatory column, re-filtered on the server so a
stale cookie cannot leave a row without its only linked cell.

The bug review caught is this codebase's recurring one: the RSC payload of
toggle A arrives inside the window of toggle B, and adopting the server there
undid B and then committed the set without it. Reconciliation now runs only
when no write is pending, pinned red against the previous code.

Municipality column ids are typed against MunicipalityListColumnId now, so the
id, the B22 description and the picker label are one key; the union member
`nivel` was renamed `level` to match the id and the ?level= filter. The
picker's own content comparison is sameIdSet, widened to string ids.
2026-07-28 08:25:16 -03:00
Francisco Ignácio Fontoura Solla
5f11960c3a B32+: share the campaign JSON route wrapper, fetch helper and cell auto-save.
`campaignJsonMutationRoute` now builds every JSON POST route under /campanha —
`isSameOriginRequest` had been copied into five of them, so the CSRF policy was
failing open by omission; a convention guard sweeps all of `src/app` and names
its three exceptions. `postCampaignJson` is the only place that spells
`credentials: 'same-origin'`, and `useCampaignCellAutosave` is the debounce /
abort / revert / flush-on-close machine the quick-edit cells shipped three
times.

The shell keeps the `aria-live` region outside the Popover/Drawer, since closing
is what commits and it was unmounting the node that would announce the result;
`variant="sheet"` took a `footer` slot, which let the signal control drop the
overlay it only kept for one button.

Two bugs the extraction made visible: a revert issued while a different payload
was in flight was dropped silently, and `change()` never cleared a past failure,
which outranks everything the live region would otherwise say.
2026-07-28 08:07:29 -03:00
Francisco Ignácio Fontoura Solla
3536eb02d3 Deliver B29: header sort and filters on the leaderships list.
Bring B15/B16-style URL sort/filter to /campanha/liderancas on the B33 mold, with shared CampaignTableHead filter slot and B18 exhaustive enum canonicalize from /simplify.
2026-07-28 01:43:44 -03:00
Francisco Ignácio Fontoura Solla
a7bc05f3a1 Adopt B42's cell-edit overlay in B34 and fix the optimistic baseline again.
The Drawer half of MunicipalityPortfolioCell now goes through
CampaignCellEditOverlay: the container switches on a `variant` prop, not on
viewport, so the pointer policy stays in CSS while the cell inherits
`initialFocus` (the hand-rolled sheet focused the first chip, a link out of the
row being edited) and the ARIA trigger attributes every other cell has.

The optimistic baseline bug was back in a shape that reads as correct. Applying
the delta functionally is not enough when the fallback is not: `current ??
municipalityIds` still read the render that created the closure, which is the
branch an undo always takes, since the server reconcile clears the optimistic
value first. Undo after a later add rebuilt the row from a set that predated it,
permanently. The baseline is a ref now, advanced synchronously in the draft
branch, and pinned by a spec verified red against the old code.

Also: revert the Drawer body gate (its saving measured as zero, and it animated
an empty box out of view), collapse the word-start matcher to two string tests
now that both sides are normalized, copy the memoized território ids before
handing them to a caller, and revalidate every changed município of an advisor
batch instead of only the last.
2026-07-27 23:59:20 -03:00
Francisco Ignácio Fontoura Solla
812e3a0bee B34: editable município chips on the leaderships list.
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.
2026-07-27 22:35:19 -03:00
Francisco Ignácio Fontoura Solla
adb1773be8 Harden the level control now that it also renders as a Drawer.
The migration put this control on a phone for the first time and it was still
mouse-sized: a 28px submit and checkbox rows with no minimum height, where the
siblings size both for touch. Closing the overlay mid-submit was worse — it
unmounts the Alert and the live region while nothing aborts the request, so a
failure went unreported; it now uses the same `reportFailure` the trend and
estimate cells do.

The rest is what the reviewers found around it: the trigger reports `aria-busy`
like every other overlay caller, `canSubmit` carries the override rule that was
stranded in the JSX, the two-level-jump check reads
`ENGAGEMENT_LEVEL_RULES.maxStepsWithoutShock` instead of a literal so E15 cannot
desync the checkbox from the rule behind it, and the route echoes
`updated.engagementLevel` rather than the requested one — with
`overrideAccess: false` Payload drops a field the actor cannot write instead of
refusing, so the request is not evidence the movement landed.

The cell's state stopped being nullable, which meant splitting it from the
route's success payload: a success always names a level, a row need not have
one. And the control joins `overlayCases`, the table that exists to catch
exactly the kind of container mistake this migration could have made.
2026-07-27 22:17:44 -03:00
Francisco Ignácio Fontoura Solla
c7001b75f0 E14: engagement levels N0-N4 and the first allocationDecision writer.
`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.
2026-07-27 21:19:33 -03:00
Francisco Ignácio Fontoura Solla
d5b4af2d98 E13: visit eligibility checklist and the tour composer.
Five conditions per município (volume, headroom, rede, janela, encaixe) with
their reason always visible, plus a staff-only composer at
/campanha/atividades/giros that turns an âncora + satélites + semente into draft
activities in one transaction. No `visita` kind and no `tour` entity:
`deputyPresent` already marks the candidate's own agenda.
2026-07-27 21:04:04 -03:00
Francisco Ignácio Fontoura Solla
211276b4e4 B32: editar Status de apoio na célula da lista de lideranças
- 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).
2026-07-27 09:56:09 -03:00
Francisco Ignácio Fontoura Solla
bcb58c2162 B33+: close the /simplify rounds on the shared list filter hook
Follows the URL when the committed query changes with no debounce pending, so
Back/Forward and the empty state's clear anchor update the search box without
overwriting text the user is still typing. That removed the last reason for
/campanha/municipios to remount its filters, so the key= (and its now orphaned
buildMunicipalityFiltersKey) is gone and the three lists share one mounting
policy — the dedup guard was dead code on municípios while it remounted.

Reuse: setSearch + scheduleSearch were always called as a pair, so they collapse
into onSearchChange (hook surface 8 -> 6 names); MunicipalityFilters reuses the
buildMunicipalityFilterHref it was reimplementing inline; dobradinhas gains the
sibling buildStateDeputyFilterHref for the four sites pinning page 1 by hand;
municípios gains the active-filter summary its mobile block was missing.

Also drops an export with no consumer that was failing knip in CI (seo.ts).
2026-07-27 09:39:15 -03:00
Francisco Ignácio Fontoura Solla
3892c2e0a4 Add remembered campaign sessions
Keep campaign JWT and cookie lifetimes aligned while revoking the active server session on logout.
2026-07-27 08:57:26 -03:00
Francisco Ignácio Fontoura Solla
98c334785e Tighten formActions guard: allowlist real exceptions and require call sites.
Also rethrow non-FormDataBoundaryError from the activity lifecycle id pre-parse.
2026-07-27 08:33:41 -03:00
Francisco Ignácio Fontoura Solla
440c15f132 C8 F4b: widen formActions guard and migrate sibling ladders.
Cover *FormActions.ts (+ taskActions allowlist) so route siblings cannot drift past the shared wrappers; migrate activity/municipality ladders onto runCampaignFormAction.
2026-07-27 08:33:41 -03:00
Francisco Ignácio Fontoura Solla
fa75c355a0 Make public SEO resilient when the metadata global is empty.
resolveSiteMetadata fails open on missing URL/text so next build no longer crashes on a fresh or poisoned metadata cache.
2026-07-27 08:20:33 -03:00
Francisco Ignácio Fontoura Solla
f06e573f15 E12: camada TI com rollups MAUP e benchmark intra-TI.
Estende territórios com cobertura/captura/classe agregadas e compara captura do município aos pares do mesmo TI no card Conta da cadeira.
2026-07-26 21:26:47 -03:00
Francisco Ignácio Fontoura Solla
06ea1ff295 B36: chips de lideranças na lista de dobradinhas
Espelha a aresta leadership.stateDeputies do B31 do lado do deputado, com célula compartilhada bidirecional, loadLeadershipOptions e casca própria de formActions.
2026-07-26 21:15:44 -03:00
Francisco Ignácio Fontoura Solla
d6455df5e7 B30: convite WhatsApp na lista de lideranças
Staff gera convite de autopreenchimento na coluna de ações da lista, reutilizando createCampaignInvite e alinhando validação de celular ao link de contato do B28.
2026-07-26 20:47:11 -03:00
Francisco Ignácio Fontoura Solla
013f167d47 Fix /campanha/liderancas 500: RSC-safe phone display
CampaignCopyableCell is a Client Component; passing formatDisplay as a
function from the leaderships RSC page broke serialization in production.
Use a preformatted displayValue string at the call sites instead.
2026-07-26 20:16:01 -03:00
Francisco Ignácio Fontoura Solla
ef3d5b6c22 B31: coluna de dobradinhas na lista de lideranças
Coluna "Dobradinhas" em /campanha/liderancas com chips clicáveis
para /campanha/dobradinhas/<slug> e edição via Popover+Command
(padrão B27, escrita via server action delta + lock, como B19).

- LeadershipRowViewModel.stateDeputies (StateDeputySummary[]) via
  loadStateDeputySummaries, substituindo stateDeputyNames.
- setLeadershipStateDeputyMembership(Record): delta com
  withPayloadTransaction + acquireTextAdvisoryLocks + revalidatePath.
- nextStateDeputyIdsAfterMembership (lib/leadershipStateDeputyMembership.ts),
  espelha nextAdvisorIdsAfterMembership.
- loadStateDeputyOptions estendido com plainName/party/slug.
- Catálogo entregue uma vez por tabela via leadershipColumns(options)
  (columns factory, mesmo precedente de municipalityListColumns).

/simplify (1ª rodada): corrige revert otimista por delta (não pelo
snapshot inteiro), remove fallback silencioso na adição, extrai
sameIdSet compartilhado com MunicipalityListAdvisorsControl (B27), e
evita leitura+revalidação supérfluas no caminho no-op da action.

/simplify (2ª rodada, pós-rebase): troca o StateDeputyOptionsProvider
(Context) inicial pela columns factory acima — a 1ª rodada tinha
validado o Context por falta da evidência de que MunicipalityList.tsx
já resolve o mesmo problema sem Context; corrige revalidatePath para
o id concreto em vez do template dinâmico '/campanha/liderancas/[id]'
(que invalidava toda ficha a cada toggle); remove non-null assertion
movendo a guarda para dentro do setCurrent updater.

Gate verde: tsc, lint --max-warnings=0, format:check, unit 530/530,
int 406/406 (incl. os 6 de campaignLeadershipStateDeputyMembership),
build. Aikido: 0 achados nos arquivos novos/editados.
2026-07-26 19:50:15 -03:00
Francisco Ignácio Fontoura Solla
2086ed50e9 B38: sidebar recolhido em tablet no shell /campanha
Rail offcanvas com cookie, default colapsado em viewport tablet e trigger visível a partir de md; fecha o item no roadmap.
2026-07-26 18:40:31 -03:00
Francisco Ignácio Fontoura Solla
8c55daffb2 B33: ordenação e filtro no header da lista de dobradinhas
- stateDeputyListUrl.ts / stateDeputyListFilters.ts: contrato de URL/filtro
  próprio (sort/dir, filtro de Partido multi + sentinela "Sem partido"),
  espelhando municipalityListUrl.ts (paginação/resolveListUrl) e
  territoryListUrl.ts (sort keys/labels)
- stateDeputyData.ts: where/sort resolvidos no banco + facet de partido
  (loadStateDeputyPartyFacet), select estreito na query principal
- StateDeputySortableHead / StateDeputyHeaderFilter / StateDeputyFilters:
  wrappers finos sobre o chrome compartilhado (shared/CampaignSortableHead,
  CampaignHeaderFilterPopover), paridade mobile incluída
- dobradinhas/page.tsx: redirect canônico em duas fases, CampaignTable
  sempre montada com caption + empty state, resumo de ordenação aria-live
- Correção pós-entrega (design review): "Sem partido" no mobile + caption
  da tabela + copy plural do empty state
- /simplify (1ª rodada): buildStateDeputyPartyOptions compartilhado, remove
  discriminante filterParam redundante, dedupe/if mortos em
  stateDeputyListUrl.ts
- /simplify (2ª rodada, pós-rebase): remove cast/type morto em
  stateDeputyData.ts, stateDeputyColumns usa StateDeputyFilterOption em vez
  de duplicar a forma, inlinea parsePartiesParam; extrai truncatedNamesLabel
  compartilhado em campaignListUrl.ts (3º call site — municipalityListFilters
  e territoryListFilters tinham a mesma função com nomes distintos)

Gate verde (tsc, lint, format, check:cycles, 396 testes, build); Aikido
0 achados nos 7 arquivos tocados nesta rodada. Docs:
docs/plans/ordenacao-filtros-lista-dobradinhas.md, docs/roadmap.md,
.cursor/rules/projects/nucleos-eleitorais.mdc.
2026-07-26 16:21:40 -03:00
Francisco Ignácio Fontoura Solla
9150092e17 B28: e-mail + celular na lista de lideranças
- colunas E-mail/Celular (copy-on-click, toast, "—" vazio) + coluna de
  Ações com WhatsApp (wa.me), no padrão de /campanha/assessores (B19)
- shared/CampaignCopyableCell.tsx nasce compartilhado já no 2º call site;
  AdvisorsTable.tsx migrado no mesmo PR para consumi-lo
- whatsAppHrefForPhone promovido a lib/phone.ts
- leadershipData.ts: contactSummary (+ email), loadLeadershipDetail
  deduplicado (reusa row.email)
- simplify: remove try/catch morto em whatsAppHrefForPhone, remove casts
  redundantes em contactSummary, displayEmail retorna string | null direto
- docs: nucleos-eleitorais.mdc, roadmap.md, plano B28 marcados entregues
2026-07-26 15:30:39 -03:00
Francisco Ignácio Fontoura Solla
b3283ee061 B24: auto-save e justificativa no popover de Tendência.
Remove o botão Salvar da lista de municípios; grava via POST political-trend sem revalidar a lista, com flush coalescido e feedback na célula.
2026-07-26 14:05:36 -03:00
Francisco Ignácio Fontoura Solla
2bbb630af6 simplify(B27): fix optimistic-state clobber, unify JSON route error shell, dedupe advisor id resolution
- MunicipalityListAdvisorsControl: stop advancing lastPropsIDsRef from a
  successful delta response — it exists only to detect a genuine external
  prop change, and writing it there made a later stale re-render of the
  pre-edit content look external and clobber optimistic state back to it.
  Also reconcile with the latest confirmed server set on the error path
  once every in-flight delta settles (previously only the success path did),
  and consolidate chip/tooltip name resolution onto the shared
  advisorEntriesFromIds helper instead of a bespoke lookup with an
  '#id' placeholder.
- Extract campaignJsonMutationErrorResponse/parseCampaignJsonRequestBody
  (src/utilities/campaignJsonMutationRoute.ts) and adopt them in both
  municipios/advisors/route.ts and municipios/expected-votes/route.ts,
  removing the duplicated body-parse + error-mapping ladder.
- Municipality.ts's validateMunicipalityAdvisors now uses
  uniqueRelationshipIds instead of re-inlining the Set+map+filter dance
  the helper was extracted for.

Verified: tsc --noEmit, lint, format:check, check:cycles all clean;
full unit suite (450 tests) and the municipality/advisor int suites pass.
knip's pre-existing payload.config.ts load error is unrelated (P3).
Playwright's spawned webServer failed to boot in this sandbox (unrelated
module-resolution environment issue, reproduced even before this diff);
manual pnpm dev boots fine and the change is covered by unit/int tests.
2026-07-26 03:51:32 -03:00