Substitui o modelo de MunicipalityUpdate (kind/semanal/urgente/nota/sinal + signalType)
por um modelo unificado: body obrigatório + polarity (boa/neutra/ruim) + urgent + adversarySignal.
- Schema: polarity enum obrigatório, urgent boolean default false, adversarySignal boolean
- Wizard: novo WizardUpdateBodyStep com campo de texto + polaridade + urgente
- Lista: controle unificado de atualização com polaridade inline
- Feed: exibe polaridade como badge colorido + indicador de urgência
- V2 status strip: atualização rápida de polaridade
- E11: queries atualizadas para polarity + adversarySignal
- Migração: 20260807_030000_unify_municipality_update (backfill polarity=neutra, urgent de kind, adversarySignal de kind+signalType)
- Testes: 18/18 integração C87, 593/593 total, 1399/1399 unitários
- Fix: testes de state deputy usavam nomes fixos que colidiam entre execuções
Issue #390 — entrada operacional staff com FullCalendar v7 route-local
(semana/mês/lista), filtros compartilháveis por município/deputado/tag,
criação pelo slot, detalhe por URL e remarcação drag/resize sob RBAC,
transação e lock por atividade.
- /campanha/atividades e giros permanecem como lista/fluxos legados
- Sem migration/collection nova; reutiliza activity de C14
- Fuso Bahia, range exclusivo, erro/revert, loading/empty testados
- Leader negado, advisor limitado pelo access atual
- E2E cobre filtros, drag, mobile 390x844 e slot prefill
- Int specs cobrem RBAC, concorrência e defense-in-depth
Impl: docs/plans/fullcalendar-agenda-campanha-impl.md
- Replace kind enum with free-form tags (text[] hasMany)
- Remove origin and deadline fields
- Status: remove rascunho/planejado, keep confirmado/realizado/cancelado
- startAt required for user-facing creates (optional for tour drafts)
- Add deputyPresent time gate (only coordinator/candidate can reschedule)
- UI: calendar-like form with tag input, no origin/deadline, no draft status
- E13 giros: tourStopRoleActivityKind → tourStopRoleActivityTags
- Migration: kind→tags with label mapping, status remap, enum recreation
- Activity texts table creation (was missing from migration history)
Closes#389
Introduce a closed-catalog URL builder that delegates to existing list/detail
href helpers, with role-based guards for leader lockdown and unrestricted-only
assessor routes. Register the tool in the AI chat and teach the system prompt
when to offer markdown links after resolving entity ids/slugs.
Closes#383
* fix(campanha): unblock dobradinhas list SSR for Assessores column
Drop CampaignTableHead description on Assessores — its hover tooltip plus
StateDeputyAdvisorRelationCell breaks production SSR with an undefined element.
* fix(test): keep int gate under Postgres connection ceiling
Cap vitest workers and raise Docker max_connections so high-core laptops
stop failing the suite with cascading "too many clients" / QueryError noise.
* fix(test): do not force eight int workers on CI
An explicit maxWorkers: 8 raised parallelism on 2–4 vCPU runners and
starved shared invite/consent advisory leases (15s timeouts). Cap only
on local high-core hosts; leave CI on Vitest's CPU-relative default.
* style(test): format vitest.config.mts
Espelho da coluna de Assessores: chips com nome do contato, popover/sheet
com Command + delta otimista e criação inline (Nome + Celular). Sem
migration — leitura reversa de leadership.municipalities no bundle e
escrita reusando as actions B34/wizard; rota POST
/campanha/municipios/leaderships via campaignJsonMutationRoute.
Fixes do /simplify (3 revisores read-only): JSDoc de normalizePathname/isListPath
documentando o invariante (homes nunca são '/' nem terminam em slash), teste de
multi-slash e inputs degenerados, comentário de isCampaignHomePath encurtado.
Sem mudança de comportamento.
Extrai isListPath + normalizePathname exportados em campaignQuickActionPaths.ts
e migra os 9 wrappers (7 públicos + 2 privados) + a 3a cópia de normalize em
campaignPageChrome.ts. Refactor estrutural sem mudança de comportamento;
derivação de regex de detalhe numérico adiada (gatilho: 3o vertical [id]).
Inclui docs/plans/escala-dry-pos-acoes-rapidas-impl.md e o fix de format
pré-existente de docs/plans/liderancas-coluna-municipios.md (bloqueava format:check).
Closes#352
- POST /campanha/municipios/advisors aceita body de criação (name) na union;
createMunicipalityAdvisorRecord cria campaignUser advisor (stub
<slug>@criado.invalid, senha aleatória) e atribui ao município na mesma
transação (cap 10 com rollback, lock municipality-advisors:{id})
- MunicipalityAdvisorCreateProvider compartilha o assessor criado entre linhas;
opção "+ Criar assessor" e chip otimista com id temporário no control
- advisorNameSchema unificado em primitives.ts; predicado de placeholder cobre
@criado.invalid (reset de senha e célula de e-mail tratam stubs inline)
- Destrava o spec e2e de municipios (falhas pré-existentes em main): key
warning do fragment trailing de MunicipalityFilters; settle-wait da animação
do Drawer no teste B136
- leadershipContactUpdateSchema + updateLeadershipContactRecord
- LeadershipContactFieldControl (lápis → input → blur-save)
- Lista e bloco Contato no detalhe
- Int tests para update, conflito de telefone e escopo
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
queueMicrotask under req.transactionID raced withPayloadTransaction
commit. Register after-commit callbacks, soft-fail missing notification
rows on send, and pin the schedule with a convention unit spec.
- 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
- 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>
- 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>
- 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>
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>
- layout=grid no botão com w-full/min-w-0 sem box-content no mobile
- li min-w-0, gap-2, data-layout=grid-3 no strip
- e2e com geometria (boundingBox) em viewport 390
- impl plan docs/plans/inicio-acoes-grade-3cols-2rows-impl.md
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
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>
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>
Insert Organizações after Lideranças in staffNav so staff can discover
the existing /campanha/organizacoes list from the structural nav.
Leaders remain on leaderNav; isCampaignNavActive already covers list,
detail, and /nova paths.
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
- Relax AllocationDecision.rationale required flag; validate in hook
- Fix empty-motivo int test to use first movement from null
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
- Move initial focus to sr-only title instead of void/body (a11y).
- Fold matchMedia mock into stubMobileViewport helper.
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
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>
- 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>
- wizardPreviousHref em wizardActionChain para todos os elos B98
- Avanço encadeado com router.push; Voltar no chrome com replace
- Testes unitários para href anterior e navegação pending
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
- Add q param to demand list URL contract and buildDemandListWhere
- Extend demandOmnibox with kind + search chips/suggestions/actions
- Search title and leadership contact name (visible list columns)
- clearDemandOmnibox drops bar filters but keeps activity deep-link
- Unit + int tests; impl plan docs/plans/demandas-omnibox-tipo-busca-impl.md
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
Adiciona dimensão Fonte ao estado de lista, where-builder, aggregate SQL
e adapter omnibox (B128), com guard isSupporterSource e testes.
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
- Block Dialog onOpenAutoFocus and Drawer initialFocus so search does not
steal focus on open (actions stay visible per B132 idle state).
- Reserve md:pr-10 on search chrome so desktop close button does not overlap.
- Enable showSwipeHandle on mobile drawer (no X button).
- Unit tests for idle state, swipe handle, and close spacing.
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
- 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>
- CampaignHomeActionStrip: grid-cols-3 no mobile; variante grid para overlay
- CampaignQuickActionsOverlay: grade no lugar da strip; altura por conteúdo;
pt-4 na busca focada; limpa query ao fechar
- CampaignQuickActionsHost: clear() em fechamento e mudança de rota
- Testes unit atualizados; impl plan em docs/plans/
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
Ensure campaignPageChrome targets campaign-page-chrome-title after rebase,
and remove useMunicipalityEstimateScenario orphaned by B129 overview removal.
Desktop chrome put title+subtitle in one text node; exact getByText
failed. Mobile subtitle matched Municípios case-insensitively. Mark the
title with data-slot and assert against that node.
Use hook.navigate so draft search cannot clobber chips; replace cmdk with
ARIA combobox keyboard contract; memoize suggestion seeds per facet snapshot.
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
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>
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>
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>
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>
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>
- Add QUICK_ACTIONS_SNAP_FULL; drawer expands on search focus and hides action strip
- Tighten dock bottom gap; restore dock/collapsed on blur per scroll position (B100)
- Widen CampaignHomeActionButton to w-[5.5rem] for two-line labels
- Client-side filter excludes current page entity from drawer search hits
- Unit + e2e coverage for full snap, exclusion, and label readability
- Allow horizontal bleed on home-actions retraction shell (overflow-y-hidden
instead of overflow-hidden) so -mx-4 is not clipped by the chrome wrapper.
- Hide HomeSummaryDelta chip and period caption when delta is 0 or null;
add shouldShowHomeSummaryDelta helper and unit pins.
Closes#150
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
- CampaignWizardShell: contentFocus prop skips title focus on form steps
- Wizard steps with inputs: contentFocus=none + autoFocus on first field
- VoteEstimateScenarioInputs compact: highlight follows activeScenario
- Municipality list compact overlay tracks focused scenario for highlight
- Unit tests for shell focus mode, compact classes, and votes step
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
Advance lastScrollTop even when search is focused so the first scroll
after blur does not false-flip snap. Re-assert dock whenever uiFocused
and the sheet left dock (handle collapse cannot hide an active search).
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
Revert handle to the top of the sheet; recalibrate collapsed/dock snaps
for handle+search and 2-line action labels; scroll ↓ collapses and ↑
re-docks; collapsed keeps a placeholder-less search peek that expands
on focus.
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
Remove the municipality step h1 via optional stepTitle on CampaignWizardShell.
Keep search input sticky under the wizard header, show hit-row skeletons
while suggest/search loads, and tighten mobile top padding when titleless.
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
- 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>
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>
Hide the visible h2 when resultKind is suggest; keep aria-label on the
section for screen readers. Search mode still shows Municípios heading.
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
Substitui as quatro labels longas de skip (votos, tendência, sinal,
liderança) por uma constante compartilhada WIZARD_CHAIN_SKIP_LABEL.
Resolvers e condições de exibição permanecem intactos (B96/B98).
Closes#131
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
- CampaignHomeActionStrip: gap-0 no ul, px-4 interno (md:px-0)
- CampaignHomeActionButton: px-2 + box-content para hit area sem encolher coluna
- CampaignQuickActionsDrawer: -mx-4 bleed na strip (espelha Início)
- Pins unitários para classes de layout B101
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
Atualiza campaignHomeActions para navegar ao próximo elo
(votos→tendência, sinal→tendência, liderança→sinal) e remove
exports de skip de votos não usados.
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>
- Reorder drawer so handle sits at the screen bottom (snap shows bottom band)
- Hide dock content when collapsed instead of clipping through search
- Attach scroll-collapse listener directly on campaign-content-scroll ref
- Replace expanded 55dvh snap with fixed DOCK height (12rem) + COLLAPSED handle
- Fix Base UI snap geometry via padding-bottom on Drawer popup
- Lift snap state to CampaignQuickActionsSnapProvider with pathname reset
- Collapse drawer when campaign-content-scroll passes threshold; reopen only via handle/pull
- Block collapse while global search is focused or active
- Dynamic content-scroll padding for dock vs collapsed peek
- Unit tests for dock default, scroll collapse, handle toggle; e2e on municipality detail
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
resolveWizardTrendSkip(entryAction) espelha resolveWizardSignalSkip:
skip só quando entryAction presente e diferente de change-trend.
WizardTrendChoiceStep e WizardTrendNoteStep condicionam skip/trailingAction.
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
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>
- Bleed do slot home-actions com -mx-4 (compensa p-4 do scroll) só no mobile
- Gap horizontal da strip: gap-3 → gap-2
- Unit pin das classes de bleed em campaignHomeLayout
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
- Remove list bullets from municipality search hits (list-none + reset)
- Tighten gap between search input and results (gap-4 → gap-2)
- Reduce mobile top padding in CampaignWizardShell (pt-3 pb-6 md:py-6)
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
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>
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>
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>
- 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>
- Collection campaignVoteSummarySnapshot (snapshot diário statewide)
- Gravação idempotente on-read no loadCampaignHomeSummary + retenção 30d
- UI: seta + magnitude ao lado do total B56 (aria-label completo)
- Reusa rollup statewide para CG/candidato; assessor vê Δ global (opção A)
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
Registry B79 para /campanha/conceitos e /campanha/perfil: catálogo vazio
para staff, busca global no drawer expandido via CampaignStaffGlobalSearch.
Leader em /perfil continua sem drawer (lockdown B89).
Closes#26
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
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>
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>
Extract CampaignGlobalSearchMount (provider + body) from home chrome
so the same home-search contract mounts in CampaignQuickActionsDrawer
when expanded. Refactor CampaignHomeStaffChrome to use the provider.
Closes#55
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
Wire the B79 drawer on /campanha/contatos for leader role with the B45
catalog (register-supporter, my-contacts). Actions use in-page hash anchors
instead of staff home-search; no drawer on Início (existing B79 gate).
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
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>
Registra /campanha/organizacoes no registry B79 com launcher
new-organization na lista; detalhe vazio até existir filtro de
lideranças por org ou município óbvio único.
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
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>
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>
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>
The four public globals (site-settings, home, metadata, privacy-policy)
spelled update access as "any authenticated user" — and campaign JWTs
authenticate against /api/*, so any campaign session (leader included)
could PATCH the public site. payloadAdminOnly matches the collection
lockdown precedent; editors lose config-global writes deliberately
(content editing stays on canManagePublishedContent collections).
Pins: identity + deny (coordinator/editor) + allow (admin fn) + anonymous
read in collectionAccessLockdown.int.spec.ts. Recurrence guard:
globalAccessConventions.unit.spec.ts bans the Boolean(user) shape in
src/globals.
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
- CampaignCellEditOverlay: dev-mode console.error when a variant=sheet
cell mounts without CampaignListSheetProvider (prod keeps the
lazy-mount fallback for resilience; unit tests exercise it, so the
invariant is development-only).
- sharedSheetHostConventions.unit.spec.ts: any tsx rendering
<MunicipalityPortfolioCell / <LeadershipStateDeputyRelationCell must
reference CampaignListSheetProvider — the next chip table cannot
reintroduce per-cell Drawers by omission.
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).
- suggestionCatalog type-imports MunicipalityTerritorialClass from
lib/territorialClassAnchors (the single source) instead of the
utilities re-export — direction inversion introduced in Pass 3.
- ESLint src/lib zone: @/utilities|components|app restriction now rejects
type imports too (allowTypeImports: false); shared types live in lib
contract modules by policy.
- engagementLevel imports DAY_MS from lib/text (fold missed in P3-K).
- searchHomeAdvisors: delete stale 'Intentional admin bypass' comment —
the call is overrideAccess: false under the staff gate.
- New guard localApiOverrideAccessConventions.unit.spec.ts: payload calls
with user must declare overrideAccess (baseline measured: 59 calls, 0
missing), and runtime dynamic import/require of payload/next server
modules is banned in utilities (dodges the server-only sweep).
* 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>
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>
- 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>
Extract buildGeometryModuleFromTopology to collapse the duplicate
topojson.feature → features → optional Map pipeline in municipality
and territory geometry submodules. F2 (ensureCachedDownload) was
already delivered in scripts/lib/cli.mjs via P3-H.
Closes#28
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
Reduz o espaçamento horizontal entre botões de ação rápida na home
do /campanha, conforme feedback pós-B72. Mantém círculo 48px, largura
w-[4.75rem], pan B67 e snap inalterados.
Closes#13
Adiciona trailing actions nas linhas de resultado da busca global:
- WhatsApp em lideranças e assessores (reusa whatsAppHrefForPhone)
- Compartilhar em demandas e atividades (Web Share nativo com fallback wa.me)
- trailingAction fora do Link da linha para não roubar navegação
Closes#9
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
* 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
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.
Root cause of the prod-build React #130 on /campanha/municipios: the
md:hidden mobile-cards tree still hydrated ~25 rows x ~5 sheet controls
(~125 eager Drawer/DrawerCloseButton instances) even on wide viewports.
Filtered ?q=... routes failed more often only because they exercise the
list with filters active — the search param itself was not the bug.
CampaignListSheetProvider hosts a single bottom Drawer for the whole
mobile section; CampaignCellEditOverlay sheet cells portal their
body/footer into it while keeping per-cell open state and autosave
scaffolds. Footers with DrawerCloseButton are only created once a sheet
actually opens. Desktop popover path is unchanged; per-cell Drawer
remains the fallback for surfaces without the provider.
Also: DrawerCloseButton (function render prop, avoids base-ui element
render in prod bundles) replaces the DrawerClose render={<Button/>}
pattern across call sites; stale e2e selectors refreshed; e2e affected
script + playwright github/list reporters for CI; use-mobile reverted
(no consumers of the gate remain).
Guardrail filed as issue #52.
Root cause of the prod-build React #130 on /campanha/municipios: the
md:hidden mobile-cards tree still hydrated ~25 rows x ~5 sheet controls
(~125 eager Drawer/DrawerCloseButton instances) even on wide viewports.
Filtered ?q=... routes failed more often only because they exercise the
list with filters active — the search param itself was not the bug.
CampaignListSheetProvider hosts a single bottom Drawer for the whole
mobile section; CampaignCellEditOverlay sheet cells portal their
body/footer into it while keeping per-cell open state and autosave
scaffolds. Footers with DrawerCloseButton are only created once a sheet
actually opens. Desktop popover path is unchanged; per-cell Drawer
remains the fallback for surfaces without the provider.
Also: DrawerCloseButton (function render prop, avoids base-ui element
render in prod bundles) replaces the DrawerClose render={<Button/>}
pattern across call sites; stale e2e selectors refreshed; e2e affected
script + playwright github/list reporters for CI; use-mobile reverted
(no consumers of the gate remain).
Guardrail filed as issue #52.
Two-step UX-1 flow on the B59 chassis with URL contract ?signalType= and optional ?entryAction= for embedded skip; reuses createMunicipalityListSignalFormAction.
Unify violation highlights with getWizardVoteViolation, share the expected-votes save error constant, and drop dead parseWizardVoteDraft plus unused municipalitySlug prop.
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.
Keep violationEditedScenario when navigating to the inconsistent scenario
so média shows warning immediately after pessimista breaks order; clear
on draft/shortcut edits. Adds regression unit tests and roadmap B76.
Staff can adjust municipality expected votes through média→pessimista→otimista steps with shortcuts, coherence warnings, and a single POST on completion.
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.
- 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
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.
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.
B58 hid the scrollbar but dropped mouse drag-to-scroll; pointer-fine
handlers pan past an 8px threshold while coarse touch keeps native pan-x.
Includes simplify pass (imperative data-dragging, coarse-pointer test)
and pin ratchet for municipalityElectoralBaseline bypass count.
Staff get six launchers with uncovered-municipalities linking to the sem_assessor list; leaders get two with contacts wired to the shared route constant.
Unifies Salvador vs RMS subgroup logic for territory rollups, aggregate class slugs, and the intra-TI capture benchmark so the MAUP-sensitive rule cannot drift across call sites.
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.
Bookmarks the canonical list recorte beside Limpar, syncs via useSyncExternalStore, and matches active URLs with a pure href comparator so the app layout never imports the heavy URL serializer.
Wire CampaignListPendingBoundary/Results on /campanha/atividades/giros so TI navigation matches list pending UX; pin contract in tourRegionPending unit spec.
After B8 F2 the choropleth already opens each Salvador ZE; drop the textual list and zoneBreakdown from the bundle, keeping the mesh provenance caveat via hasZoneMunicipalities.
Staff sees a Flag + tooltip on the name column and mobile card instead of the destructive “Prioritária” chip, with copy centralized in municipalityLabels.
`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.
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.
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.
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.
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.
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.
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.
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.
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.
`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.
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.
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.
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.
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.