Commit graph

143 commits

Author SHA1 Message Date
Cursor Agent
479af4f2ef style: prettier bahiaGeometryModuleFactory 2026-07-31 01:42:01 +00:00
Cursor Agent
8188c9a386 refactor(B5): reuse BahiaFeature type and drop wrapper casts 2026-07-31 01:41:00 +00:00
Cursor Agent
ce67738485 refactor(B5): shared geometry module factory for mun/TI (F3)
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
2026-07-31 01:38:03 +00:00
Cursor Agent
6c8d7d418c B78 — Indicar carregamento na navegação do wizard
Reusa CampaignListPendingBoundary no layout autenticado para compartilhar
useTransition entre o header mobile e o conteúdo do passo. Voltar, X, Pular,
seleção de município e tiles de passo passam por CampaignWizardNavLink;
o corpo dima com aria-busy e o header mostra skeleton nos slots de título.

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

Closes #14
2026-07-31 00:24:39 +00:00
Cursor Agent
fcbc463990 fix: format + wire WIZARD_TREND_SAVED_MESSAGE for knip 2026-07-30 23:03:25 +00:00
Cursor Agent
f0f3d4534d B64 — Wizard mudar tendência (grid + justificativa + skip) 2026-07-30 23:03:25 +00:00
Cursor Agent
8ff9e20a88 fix(B55): defer share strategy to click + stabilize unit tests 2026-07-30 22:47:03 +00:00
Cursor Agent
b96bf190e8 B55 — Busca global: ações de linha (WA e compartilhar)
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
2026-07-30 22:47:03 +00:00
Francisco Solla
142b0963e0 B54 — Busca global: layout responsivo dos resultados (#60)
* feat(campanha): responsive grid and viewport cap for home search results (B54)

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

Closes #8

* fix(campanha): un-export knip-unused home search budget helpers
2026-07-30 18:45:44 -03:00
Francisco Ignácio Fontoura Solla
9eafe6081c feat(campanha): add Demandas group to home global search (B53)
Staff can find demands by title from the Início omnibox with municipality and status in the secondary line, scoped by campaignDemand access.
2026-07-30 12:39:35 -03:00
Francisco Ignácio Fontoura Solla
de2d457cf9 chore: delete dead exports flagged by knip (CI main estava vermelho no drift) 2026-07-30 05:28:41 -03:00
Francisco Ignácio Fontoura Solla
8c7633da4a B63+: wire wizard signal steps to B75 chrome and DRY skip. 2026-07-30 04:01:49 -03:00
Francisco Ignácio Fontoura Solla
09fa21d4b3 B63: wizard registrar sinal (grid de tipos + passo de texto).
Two-step UX-1 flow on the B59 chassis with URL contract ?signalType= and optional ?entryAction= for embedded skip; reuses createMunicipalityListSignalFormAction.
2026-07-30 04:01:48 -03:00
Francisco Ignácio Fontoura Solla
29f6b32e91 refactor(campanha): simplify B70 wizard loader and tile a11y.
Lean tile loader avoids org/deputy fetches; server sorts names; info button is no longer nested inside the tile button.
2026-07-30 03:51:25 -03:00
Francisco Ignácio Fontoura Solla
dbd9d0fa3e feat(campanha): ship B70 leadership update wizard (grid + short form).
Replaces the post-municipality stub with a full wizard: tile grid, contact+leadership writes, entryAction skip, and Continuar after save.
2026-07-30 03:51:25 -03:00
Francisco Ignácio Fontoura Solla
625f25c984 B77+: simplify vote wizard helpers after single-screen delivery.
Unify violation highlights with getWizardVoteViolation, share the expected-votes save error constant, and drop dead parseWizardVoteDraft plus unused municipalitySlug prop.
2026-07-30 03:40:20 -03:00
Francisco Ignácio Fontoura Solla
f2bdb1ce3c B77: replace vote wizard ?cenario= ritual with single-screen editor.
Three scenarios on one step with confirm-only validation, legacy URL redirect, and updated tests/docs.
2026-07-30 03:31:57 -03:00
Francisco Ignácio Fontoura Solla
037785b280 B75: wizard mobile header chrome on Mandate Red bar.
Publish flow navigation from CampaignWizardShell via layout-level chrome so wizards no longer stack a second sticky header under the app bar.
2026-07-30 03:26:54 -03:00
Francisco Ignácio Fontoura Solla
37a7062428 B69: simplify the leadership model to exclusive + drop sector/consentNote.
Organization stays by product decision (political branching, e.g. SindMed);
the leadership gains `exclusive` (default true) with list badge and form
checkbox, and sector/sectorNotes/consentNote leave the collection, zod,
forms, list filters/sort, invite autofill and redemption.
2026-07-30 02:32:56 -03:00
Francisco Ignácio Fontoura Solla
03bcf2e719 B71: polish home search results (bleed hover, list-none, sentence-case headings).
Centralize layout tokens in homeSearchUi.ts so hit rows fill the scroll padding on hover/focus and group lists drop public-site bullet leakage.
2026-07-30 01:27:05 -03:00
Francisco Ignácio Fontoura Solla
34e5c6ed42 B61: wizard vote step with scenario passes and batch save.
Staff can adjust municipality expected votes through média→pessimista→otimista steps with shortcuts, coherence warnings, and a single POST on completion.
2026-07-30 01:16:19 -03:00
Francisco Ignácio Fontoura Solla
a8cb771303 B62+: flatten adversary signal to boolean and harden leader redirect e2e.
The simplify pass dropped the vestigial object wrapper on E11 input and aligned the leader home test with the retrying toHaveURL idiom used elsewhere.
2026-07-29 23:33:46 -03:00
Francisco Ignácio Fontoura Solla
120ed8d9b5 B62: simplify the signal model to type + text (drop fonte/triangulado).
Drops signalSource/triangulated from municipalityUpdate (migration
simplify_municipality_signal_fields), rewrites signal-type labels for the
field team, and treats any adversary signal as confirmed in E11 (P1
triage 1). Also fixes the stale post-B43 leader-home e2e and lowers a
drifted bypass pin.
2026-07-29 23:24:00 -03:00
Francisco Ignácio Fontoura Solla
f6b73b3e63 B60: wizard municipality search step with shared staff loader.
Staff wizards pick a municipality via word-start search, auto-advance with ?municipio=, and reuse searchStaffMunicipalityHits extracted from B48.
2026-07-29 23:10:06 -03:00
Francisco Ignácio Fontoura Solla
67058d0027 B51: add Atividades group to staff home search.
Staff can find activities by title with scoped access, municipality+date secondary, and word-start matching on the existing home-search POST.
2026-07-29 22:57:02 -03:00
Francisco Ignácio Fontoura Solla
38459b7426 B52: add Dobradinhas group to staff home search.
Staff can find state deputies by name or party with municipality counts in the global Início search, mirroring the B49/B50 loader and UI pattern.
2026-07-29 22:52:35 -03:00
Francisco Ignácio Fontoura Solla
2c80d9b54f B49: home search Lideranças group on staff Início.
Extends POST /campanha/home-search with leadership hits (word-start, advisor scope, cap 25) and HomeSearchLeadershipGroup below Municípios.
2026-07-29 22:14:30 -03:00
Francisco Ignácio Fontoura Solla
0e73d47a62 B50: home search Assessores group for unrestricted staff.
Word-start on name, municipality count on the row, shared HomeSearchHitRow; POST /campanha/home-search aggregates advisors alongside municipalities.
2026-07-29 22:09:42 -03:00
Francisco Ignácio Fontoura Solla
e733efbe42 B68: curated municipality suggestions on empty home search focus.
Staff see role-ranked shortcuts (advisor portfolio by signal age; CG/candidate by alta priority deficit) via POST mode suggest, without treating empty query as search.
2026-07-29 22:06:02 -03:00
Francisco Ignácio Fontoura Solla
dd23d2a44a B59: chassis do wizard de campanha (shell, rotas /acoes, hrefs no Início).
Staff ganha CampaignWizardShell, rotas /campanha/acoes/<slug> com passo ponte até B60, e strip do Início com links nos fluxos wizard.
2026-07-29 22:02:04 -03:00
Francisco Ignácio Fontoura Solla
274cde65a7 B58: polish Início action strip and centralize coarse press state.
Short staff labels, hidden scrollbar, circle feedback on hover/press, and long-press pressing lives in useCampaignLongPress for reuse.
2026-07-29 16:10:24 -03:00
Francisco Ignácio Fontoura Solla
05072547cd B48: home search results for municipalities and identity territories.
Staff Início posts to /campanha/home-search with scoped word-start matches, shared readout component, and simplify hardening (messages, fetch busy, malformed response guard).
2026-07-29 16:05:56 -03:00
Francisco Ignácio Fontoura Solla
7bb7b5e261 B47: staff-only home search input with focused mode hiding action strip.
Debounced omnibox chassis and empty results region for B48+; no hit providers yet.
2026-07-29 15:31:46 -03:00
Francisco Ignácio Fontoura Solla
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
f03a28fd59 B44: UX-1 home action button and horizontal strip primitives.
Tooltip on fine pointer and long-press drawer on coarse without mounting on Início yet (B45).
2026-07-29 14:20:52 -03:00
Francisco Ignácio Fontoura Solla
2b3d688be3 Simplify E12+ Metropolitano peers extraction.
Centralize sub-row slug helper, drop one-line filter wrappers, and scope E12 sub-row rollups to the Metropolitano region inputs.
2026-07-29 08:24:19 -03:00
Francisco Ignácio Fontoura Solla
be2cbfbb43 Extract Metropolitano TI peer rules into lib for E12+.
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.
2026-07-29 08:15:09 -03:00
Francisco Ignácio Fontoura Solla
158ef22c2c Resolve Salvador ZE in the staff "Onde estou" card (B14 / B8 F2-D1).
Point-in-polygon on the zone mesh opens the accessible ZE directly, with nearest-zone fallback within 30 km before the filtered list.
2026-07-29 07:56:58 -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
ac2390587e B18: saved municipality list filters in localStorage and sidebar submenu.
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.
2026-07-28 19:25:12 -03:00
Francisco Ignácio Fontoura Solla
38c310a1db Lift selected options to the top in shared header multi-filters.
Freeze order on Popover open so rows do not slide under the pointer, and keep the same presentation on the mobile NativeSelect without a snapshot.
2026-07-28 18:37:05 -03:00
Francisco Ignácio Fontoura Solla
394826c2f4 Rename municipality list goal column label to Cobertura.
Shorten the list header, mobile card, and sort summary via the B17 label SSoT while keeping glossary and KPI strips on the long metric name.
2026-07-28 14:30:31 -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
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