Commit graph

155 commits

Author SHA1 Message Date
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
Francisco Ignácio Fontoura Solla
7c50e29aa7 B27: combobox + chips + auto-save no popover de Assessores da lista de municípios
Replaces the checkbox-list + "Salvar assessores" button in the
Assessors popover (/campanha/municipios) with removable chips + a
Command combobox, delta-based auto-save (one advisor add/remove per
request, never the whole array), and optimistic UI with request
sequencing to resolve out-of-order responses correctly.

- New POST /campanha/municipios/advisors JSON route (mirrors
  expected-votes/), backed by setMunicipalityAdvisorMembership(Record)
  in actions/municipality.ts (text advisory lock + transaction).
- nextAdvisorIdsAfterMembership moved to
  src/lib/municipalityAdvisorMembership.ts with unit tests.
- Shared isSameOriginRequest (src/utilities/sameOriginRequest.ts) and
  uniqueRelationshipIds (src/utilities/relationship.ts) helpers,
  de-duplicating the same-origin CSRF check and the
  Set+map(relationshipId)+filter pattern across advisor call sites.
- MunicipalityListAdvisorsControl.tsx rewritten: chips, Command
  combobox, per-item pending affordance, rollback on error, and a
  request-sequence guard so only the most recently sent delta's
  response is adopted when concurrent optimistic updates settle.
- e2e coverage for the combobox auto-save flow; setup prewarms the new
  POST-only route to avoid Playwright dev-server HMR aborting the
  first request.
- Plan and roadmap marked as delivered (B27).
2026-07-26 03:31:51 -03:00
Francisco Ignácio Fontoura Solla
63fe4431bf E10: classificacao territorial relativa (Reduto/Expansao/Manutencao/Marginal/Sem base) sobre o artefato TSE, com coluna+sort+filtro na lista, badge no card da conta da cadeira e na capa do dossie; seam generico cellTooltip na CampaignTable e promocao do CampaignHoverTooltip para shared 2026-07-25 21:59:20 -03:00
Francisco Ignácio Fontoura Solla
a9e1900290 B21: cria página própria dos territórios
Leva a leitura dos 27 Territórios de Identidade para o sistema de listas com estado canônico na URL e remove o painel duplicado do Início.
2026-07-25 21:50:24 -03:00
Francisco Ignácio Fontoura Solla
619666dd51 C13 pos-simplify: fecha os residuos da varredura e estreita os guards
O rename mecanico deixou um defeito silencioso e alguns fosseis: o fixture de
recentVisits perdeu o ${index} (todas as entradas com o mesmo label, invisivel
porque o label nao era asserido) e access/shared.ts guardava "action plan" em
prosa, forma que o guard de vocabulario nao cobria. O padrao agora tolera espaco
e a allowlist encolheu: o comentario de campaignMigrationReconciliation cita
activity e o arquivo saiu da excecao.

Na migration, a entrada action_plan_municipality_idx era inalcancavel (nenhuma
migration jamais criou esse nome) e saiu, deixando up() e down() com os mesmos
48 pares; as duas buscas de FK passam a usar to_regclass, que retorna NULL em vez
de lancar -- a unica consulta que contradizia a invariante do proprio cabecalho.

O detalhe de organizacao para de derivar o rotulo de status a mao e reusa
ActivityStatusBadge, com status tipado como ActivityStatus no loader.

Debitos do /simplify registrados: C8 F4b (o guard de formActions so inspeciona
arquivos chamados exatamente formActions.ts) no plano do C8; defers e descartes
com gatilho no plano do C13.
2026-07-25 21:29:32 -03:00
Francisco Ignácio Fontoura Solla
7b93c232ff C13: renomeia "Plano de Acao" para "Atividade" em toda a vertical /campanha
Collection actionPlan -> activity (labels "Atividade"/"Atividades"), campo
campaignDemand.actionPlan -> activity, rota /campanha/planos ->
/campanha/atividades (criacao em atividades/nova), sidebar "Atividades" e
rotulos alinhados a entidade ("Tipo de atividade", "Origem da atividade",
"Resultado da atividade"). Valores de enum kind/status/origin inalterados.

Schema movido por migration data-preserving escrita a mao
(20260725_213000_rename_action_plan_to_activity): 4 tabelas, 3 enums, 2
colunas, 39 indices (4 pkeys inclusos), 14 FKs e 2 sequences, cada statement
guardado na existencia do objeto de origem, com down() simetrico. O gerador do
Payload emitiria DROP+CREATE e nao ve o indice parcial escrito a mao
action_plan_upcoming_start_at_idx, entao a lista veio de
pg_indexes/pg_constraint/pg_type/pg_sequences e o snapshot .json foi derivado
do anterior por transformacao textual -- validado por migrate:create __probe
sem mudanca de schema. A mesma migration alinha os 8 indices fosseis %plaza%
que a remodelagem de municipios deixou (renomeou as colunas plaza_id, nao os
indices).

Guard: o describe "banned campaign terminology" de
codebaseConventions.unit.spec.ts virou tabela de termos varrendo src+tests+
scripts (migrations fora -- historia congelada), banindo os compostos
actionPlan, a copy "plano de acao" e a rota antiga, mas nao plan/plano isolado
(colidiria com os planos de assinatura da Fase 2). Allowlist: o proprio spec,
scripts/generate-remodel-municipalities-migration.mjs e o comentario historico
em campaignMigrationReconciliation.int.spec.ts.

Plano: docs/plans/renomear-plano-acao-para-atividade.md
2026-07-25 20:50:38 -03:00
Francisco Ignácio Fontoura Solla
228eb36e90 B26: registrar sinal tipado a partir da célula Último sinal na lista de municípios.
Fecha o laço da fila E9 — Popover/Drawer cria municipalityUpdate kind sinal sem sair da varredura; submit explícito multi-campo; revalidate da lista.
2026-07-25 20:42:01 -03:00