Commit graph

210 commits

Author SHA1 Message Date
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
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
7b9dedf538 Document defer trigger for explicit multi mode on header filters.
The checkbox-row gate is enough for current consumers; note when to add a prop.
2026-07-28 18:37:28 -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
0bfaa74f19 Add emendas parliamentary-amendments discovery cycle (ciclo 3): 4-axis compendium, four synthetic personas, two-round interviews with cross-examination, consolidated report (kernel, 12 ranked pains, OST mapped to /mandato-/campanha-/comunicacao, anti-goals, 14 bets), and the real-interview script for the gabinete's planilha keeper.
Research only — no roadmap items, no verticals, no code; validation with the real person gates everything (notebook: .cursor/rules/projects/emendas-mandato.mdc).
2026-07-28 18:05:35 -03:00
Francisco Ignácio Fontoura Solla
73b9ab84ed Remove the redundant zone list under the campaign map.
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.
2026-07-28 16:00:14 -03:00
Francisco Ignácio Fontoura Solla
524dbb2198 Replace priority badge with flag indicator on municipality list.
Staff sees a Flag + tooltip on the name column and mobile card instead of the destructive “Prioritária” chip, with copy centralized in municipalityLabels.
2026-07-28 14:49:33 -03:00
Francisco Ignácio Fontoura Solla
ecb20c4197 Document deferred mobile card label parity in cobertura plan. 2026-07-28 14:31:00 -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
56a4e8f31f Run the campaign-finance discovery cycle (candidate's request): compendium, three synthetic personas, two-round interviews, consolidated report, and the script for the real finance-manager session.
No product items registered yet — the 2026-07-29 session with the real gestora decides whether the finance track opens; pre-validation P0-P2 assessment lives in the project notebook.
2026-07-28 13:03:03 -03:00
Francisco Ignácio Fontoura Solla
fb64e36537 Record the B40 closing triage without opening an ID.
Nothing left from the reviewers scored high enough to earn a roadmap item, so
the four discards are written down with their reasons instead — a discard whose
reason is missing comes back next session dressed as an obvious win. The one
worth keeping is the enrollment toast's third spelling of the failure copy: it
is deliberate, because that toast has no retry affordance and ending the
sentence in "Tente novamente." would be a false instruction.

Three engineering deferrals gained the trigger they lacked (shared HMAC core,
an optional `bodySchema` on the route shell, promoting `postJson`) — all three
are second-call-site abstractions, so the trigger is the third.

Also corrects two statements this delivery falsified: the notebook still said
`postJson` translates `SyntaxError`, which the simplify pass deleted, and the
roadmap's B40 item carried the pre-optimization 130 kB that its own header line
had already restated as 126 kB.
2026-07-28 12:34:59 -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
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
5fb77b46bd Record the B17 closing triage as B17+.
Two findings outlived the simplify pass, both structural. Hiding a column does
not switch off the query that exists only for it: the two relation catalogs are
loaded inside the `Promise.all` and the cookie is read after it, and since
`cookies()` does no I/O, the fix is statement order rather than a new await.
And the "Colunas" button disappears with the table on the three surfaces that
swap the whole table for an empty block instead of using `CampaignTable`'s
`empty` prop, so filtering to zero rows removes a control the filter never
touched.

Five findings were discarded with the measurement or the reason beside them, and
three deferred with a trigger — the shared Popover chrome, the e2e staff fixture
and a shared "load unless hidden" helper all sit at two call sites.
2026-07-28 09:23:04 -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
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
c657a9be16 Record the B32+ closing triage on the two parent plans.
The state-deputy cell's live region was hoisted out of its Popover by this
delivery, so B34+ F2 must not reintroduce it inside the overlay — and with six
callers on the shared shell, that cell's trigger/Popover half is adoption now
rather than extraction, leaving the chip machinery as the real work. The
declined `AdvisorDebouncedTextCell` reuse gets the trigger it was missing: a
second debounced auto-save writing through a server action.
2026-07-28 08:11:07 -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
aa11262104 Record B29 post-simplify deferrals on the parent plan.
Capture triage gates for filter-definitions and list select depth without opening a new roadmap ID.
2026-07-28 01:44:47 -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
9f3b128ec2 Cross-reference the sibling catalog-bundle debt from B33+.
The rebase on main brought B33+ into view: it registers the same static Bahia
catalog reaching /campanha/municipios' client bundle. Same class, opposite
cause — there a predicate drags the module in by accident, here the cell needs
the data — so the fixes differ, but the First Load JS bench is shared.
2026-07-27 22:39:43 -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
93b58a9b3c Register the E14 closing triage on B32+.
Three debts, no new ID. The fifth JSON control and route arrived without
touching F1 — the level movement is an explicit submit, not a debounce — which
is exactly the escape clause F2/F3 were parked behind, so they stop being
conditional. F2 also changes character: the same-origin check that
`isSameOriginRequest` documents itself as owning is copied into five routes, so
the one that forgets the line fails open. That is not boilerplate and is not
cuttable by appetite.

F5 is B42's own deferred trigger firing: it parked the footer slot on "a second
control needing explicit submit in a Drawer", and this is that control. What the
deferral could not know is that the shell pins "Fechar" while "Registrar
movimento" sits below the fold, so the action that discards a typed rationale is
the reachable one.

Discarded with reasons in the triage: `new Date()` during render (the server
re-runs the rules and a 409 is a designed state), and `levelNote` in the 435-row
select, already written down in E14's own plan next to `politicalTrend.note`,
which has the same shape and must be fixed with it.
2026-07-27 22:24:34 -03:00
Francisco Ignácio Fontoura Solla
8d6c8e3313 Move the level control onto B42's shared cell-edit overlay.
B42 landed on main during this session and extracted `CampaignCellEditOverlay`
— Popover on `md+`, Drawer below it, where a popover fights the virtual
keyboard. The rebase auto-merged E14's "Nível" block into what had become the
Drawer, which compiles and would have opened a popover inside a sheet, so the
control now takes `variant` like its three siblings. The container's Popover
branch is point for point what E14 had hand-written, so the desktop is
unchanged; on touch, where there is no hover to carry it, the trigger now
spells the level out instead of showing the bare numeral.
2026-07-27 21:24:45 -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
a7feb6b26e Register the E13 review debts on the roadmap.
The one finding the /simplify skipped for the wrong reason becomes a fill-in:
the tour composer signals a território change only on the control, and the
pending pieces it dismissed as list-coupled are a Provider, a div and a hook
with a local fallback. The batched goal-account loader stays a trigger on the
E13 plan — the planner and the list diverge on missing data on purpose.
2026-07-27 21:12:05 -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
2983011bfe Register the B42 review debts on the roadmap.
The aria-live region that unmounts with the overlay becomes B32+ F4, and the
catalog bundle debt in B33+ stops being conditional now that B41 and B42 both
shipped on that route without paying it.
2026-07-27 20:54:12 -03:00
Francisco Ignácio Fontoura Solla
59d89ce287 B42: Drawer quick-edit and a tappable card on the mobile municipalities list.
Tendência, Assessores and Votos estimados now share one container
(CampaignCellEditOverlay: Popover on md+, bottom Drawer below), the card drops
to the table's density, and the whole card navigates through a stretched link
instead of a second "Abrir município" button.
2026-07-27 20:49:41 -03:00
Francisco Ignácio Fontoura Solla
6de807e288 Note B41 critique deferrals on the parent plan.
Edge fade and sticky-cell hover stay Adiado with explicit triggers.
2026-07-27 10:38:06 -03:00
Francisco Ignácio Fontoura Solla
6c0c5e87ce B41: horizontal scroll and sticky Município on the municipalities list.
Mirror TerritoryList so wide staff columns can scroll on md+ without losing the row identity.
2026-07-27 10:16:16 -03:00
Francisco Ignácio Fontoura Solla
b6a8b64a66 Registra B32+ (débito de auto-save pós-B32)
capture-review-debts do fechamento do B32: MunicipalityListExpectedVotesControl
(B27), MunicipalityListTrendControl (B24) e LeadershipListSupportStatusControl
(B32) são o 3º call site do mesmo scaffold de auto-save por debounce, gatilho
já registrado nos três planos. Fill-in de engenharia com auditoria antes de
extrair (precedente B33+): docs/plans/escala-dry-pos-b32.md.

Outros achados do /simplify (flush-on-close, comentário/summary morto em
SupportStatusBadge) já foram corrigidos no commit do B32; null→a_abordar e o
findByID extra em updateLeadershipInternal são decisões já documentadas,
descartadas desta triage.
2026-07-27 10:04:30 -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
abe37cbc5c B33+: shared list filter navigation hook
Extract the debounce/dedup/transition scaffold that StateDeputyFilters,
TerritoryFilters and MunicipalityFilters each reimplemented into
useCampaignListFilterNavigation, and unify the two behaviors that had
silently diverged: touching a filter mid-typing used to discard the
pending search on dobradinhas and territorios, and the debounce timer
captured a stale state, so a sort clicked within its window was reverted
a second later. Also folds the six copies of the pending-transition
fallback into useCampaignListTransition and drops a redundant aria-live
that made the municipality summary the third live region on its page.
2026-07-27 09:05:08 -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
edd2172bce Format F4b plan and TECH-DEBT after Prettier check. 2026-07-27 08:34:57 -03:00
Francisco Ignácio Fontoura Solla
b9d697373c Document F4b simplify follow-ups and deferred form-action guard triggers. 2026-07-27 08:34:30 -03:00
Francisco Ignácio Fontoura Solla
3c162c5250 Fix leftover rebase conflict markers in roadmap and project notebook. 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
7eb3ca20a6 Format the metadata resilience plan for Prettier. 2026-07-27 08:21:25 -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
9afa474879 Register the B8+ review debts from capture-review-debts.
Dos 20 achados do /simplify, 12 entraram na entrega e 3 ja estavam
cobertos (o modo `territory` pertence ao E12 por rabbit hole do plano, a
shoelace duplicada e descarte declarado, e o B5 F2 ja contava os 5 call
sites dos helpers de CLI). Sobraram dois fatos sem registro.

O revisor de perf mediu que `quantize` aplica um unico numero de digitos
aos dois eixos: a grade de ~103 m e exata em longitude e a latitude segue
o aspect ratio do bbox (102,8x109,2 m no estado, 102,9x80,1 m em
Salvador). Corrigir exige `{scale, translate}` explicito e reescrever os
tres artefatos, perdendo a prova de byte-identidade da malha estadual por
um erro de ~9% num eixo num mapa que para em maxZoom 10 — entao vira
gatilho no plano-pai, nao item de roadmap.

E o B8+ respondeu por precedente uma pergunta em aberto do B5 F2: com
`scripts/lib/topology.mjs`, o helper de cache da F2 pousa em
`scripts/lib/` em vez de abrir um segundo endereco. A recomendacao antiga
(`src/lib/cliCachedDownload.ts`) fica rejeitada: `src/lib/` e client-safe
por contrato e nao e lugar de codigo que faz mkdir/fetch/writeFile.

Sem novos IDs de roadmap.
2026-07-27 02:04:09 -03:00
Francisco Ignácio Fontoura Solla
2c30944593 B8+: escala das malhas em unidades reais, contrato do map key e degradacao
F1 — a tolerancia de simplificacao e os digitos de quantizacao viraram
politica em unidades de solo em scripts/lib/topology.mjs, consumida pelos
dois scripts de geometria. Os quantis 0.35 copiados cortavam as 19 ZE de
Salvador a ~0.6 m2 e os 417 municipios a ~1.78 km2; com tolerancia
absoluta e a grade de ~103 m do estado, a malha das zonas caiu de 480 para
30.2 pontos/feature. bahia-municipalities segue byte-identico e o tripwire
de bytes deu lugar a pontos/feature.

F2 — featureMapKey passou a ser asserido sobre features reais das tres
malhas comprometidas (417 codarea, 27 code, 19 slug), e o comentario de
MAP_KEY_PROPERTIES agora diz o que o codigo faz: as propriedades sao
mutuamente exclusivas, nao ordenadas por especificidade.

F3 — a malha das zonas e um detalhe sobre o mapa, nao o mapa: se o chunk
falha, Salvador cai no poligono municipal que ja esta desenhado embaixo e
o resto da Bahia continua funcionando, com o motivo logado. A ressalva de
aproximacao saiu do gate de comparacao e passou a depender de
bundle.hasZoneMunicipalities, derivado no servidor de municipality.kind.

F4 — orfaos deletados: os dois acessores sem chamador, isZoneMapKey (o
servidor ja sabe a regra), o wrapper simplifyTopology e o helper
polygonCount com um branch inalcancavel. MunicipalityMapNavigation
colapsou em string | undefined e MunicipalitiesByMapKey em
Record<string, string>.
2026-07-27 01:59:36 -03:00
Francisco Ignácio Fontoura Solla
dd8c4d1d27 Fix Prettier formatting in roadmap after users.roles docs update. 2026-07-27 01:54:58 -03:00
Francisco Ignácio Fontoura Solla
bb5ac362ed Add users.roles so Payload /admin can separate editors from admins.
Editors publish post/tag/media without PII or campaign access; existing users backfill as admin.
2026-07-27 01:54:29 -03:00