Commit graph

17 commits

Author SHA1 Message Date
Francisco Solla
aca3ea6188 feat(C153): catálogo de falas do Solla — schema, access, import idempotente e busca
Collections speech/speechSegment no grupo Comunicação, papel communicator
(leitura restrita) e índice trigram normalizado para a busca por segmento.
Import idempotente da Câmara (discurso→evento→trecho→VOD→ASR) com facetas
gazetteer+LLM, proveniência e preservação de curadoria manual; parser de
quem presidia; reusa a lógica pura do piloto C152 via scripts/lib.

Related #955
2026-09-13 02:08:55 -03:00
Francisco Solla
ba77347616 fix(campaign): D13 — align supporter_import_batch.actor_id schema drift (Closes #643)
Make actor_id nullable to match its ON DELETE set null FK; set SupporterImportBatch.actor required:false so config agrees with schema. Keep the deleteCampaignUserImportBatches hook + hand-deletes as explicit cleanup (no schema cascade, per webauthn precedent). Adds int coverage for account-delete with an orphan batch.
2026-08-24 22:39:03 -03:00
Francisco Solla
6c582bbb3f fix(contact): C120 — serializa o append de telefone no reuso com lock de ficha (Closes #663) 2026-08-24 19:03:29 -03:00
Francisco Solla
3f74b696e1 feat(C141): perfis de permissão por assessor — Visão × Edição configuráveis, enforcement fail-closed (Closes #105)
(cherry picked from commit 60857ad34d)
2026-08-20 14:25:01 -03:00
Francisco Solla
6de8fad8f6 feat(C112): múltiplos telefones por pessoa — Contact.phones array (ordem = prioridade)
- Contact.phone (text único) → phones (array field Payload, subcampo value):
  fonte única; principal = phones[0]; beforeValidate normaliza, filtra vazios,
  rejeita inválido e duplicado DENTRO da ficha (C111 entre fichas preservado)
- migration add_contact_phones_array: backfill phone → phones[0] (gen_random_uuid)
  antes do drop da coluna + índice em value (hand-edit documentado; snapshot .json
  gerado mantém as colunas C99/C100 já existentes)
- primaryPhoneOf / phoneValuesOf / reorderWithPrimaryPhone em src/lib/phone.ts
  (ordem = prioridade; inline edit = seta o principal preservando o resto)
- findOrCreateContactByPhone aceita phones[] (dedupe pelo primário; reuse faz
  append dos faltantes; locks em todos os números digitados)
- sync conta→ficha (CampaignUser): phone da conta faz APPEND quando a ficha não
  tem (nunca reordena nem remove); tombstone LGPD substitui TODOS os telefones
- convite redemption: telefone digitado vira principal, demais preservados
- import CSV: preview sinaliza compartilhado também por número secundário;
  bulk insere contact_phones (JOIN nas leituras, uuid no insert)
- busca por telefone (apoiadores, pickers, combobox) casa qualquer número
- UI: PhonesFieldEditor compartilhado (repetível mascarado, remover, ↑/↓,
  modo inline com Salvar) nas 5 superfícies — liderança (form/ficha/lista),
  apoiador (form/ficha), dobradinha (ficha), assessor (ficha), wizard
- UI pública (petição/WhatsApp) intocada: zod e forms ficam com phone;
  as ações mapeiam phone → phones[0]
- testes: spec novo campaignMultiPhones (aceite completo), specs C111/C99
  atualizados, e2e wizard/apoiador ajustados
2026-08-11 10:47:02 -03:00
Francisco Solla
162a518416 feat(C111): telefone não único — duas pessoas podem compartilhar o mesmo celular
Contact.phone deixa de ser único em enforcement (nunca houve constraint DB):
- remove o hook enforceUniqueContactPhone e os asserts assertContactPhone*
  (conflicts/ambiguity) — os 5 fluxos de escrita de pessoa salvam com número
  compartilhado; rename contactPhoneInvariant.ts -> contactPhoneLocks.ts
  (locks passam a significar serialização, não unicidade)
- findOrCreateContactByPhone: 0 fichas cria, 1 reusa (dedupe C6/C99),
  2+ cria ficha nova (a identidade da pessoa em criação é a ficha nova)
- convite autofill/login: ficha ancorada pode compartilhar o phone; o lado
  conta permanece fail-closed (username DB-unique; check de 2+ contas)
- import CSV: preview sinaliza telefone_compartilhado (resolução manual);
  confirm/bulk abortam fail-closed se um phone ganhar 2+ fichas (race)
- fix de achado de execução: supporter_import_batch.actor_id NOT NULL com
  FK ON DELETE set null derrubava o delete de campaignUser com batch
  órfão (23502) — CampaignUser.beforeDelete limpa batches + fixture
  cleanup descobre/deleta batches dos accounts owned
2026-08-10 23:03:05 -03:00
Francisco Solla
27daa1339e feat(C99): toda conta campaignUser aponta para uma ficha Contact; lideranças ganham assessores responsáveis 2026-08-09 19:12:35 -03:00
Cursor Agent
de67acf41b D2 — Push + sino in-app para /campanha
- Collections Notification e PushSubscription com migration
- Sino no header (mobile + desktop) com drawer de avisos lidos/não lidos
- Hooks em municipalityUpdate, supporter, activity e convite aceito
- Push Web via web-push + handlers no service worker /campanha/sw.js
- Opt-in LGPD em /campanha/perfil (chave campanha-notificacoes-push)
- Testes unit/int de contrato e access

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-07-31 01:51:41 +00:00
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
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
e6f7c2e70f Tooling: prettier como autoridade de formatacao (organize-imports deterministico, scripts format/format:check, normalizacao mecanica de 312 arquivos; artefatos gerados e migrations excluidos via .prettierignore) 2026-07-25 17:22:20 -03:00
Francisco Ignácio Fontoura Solla
cf6ae6c47c Pass 2 W2: correcoes de fronteira lib/utilities e client boundary
- helpers puros movidos utilities->lib: slug, phone, wordStartFilter, voteEstimate (9 inversoes lib->utilities zeradas)
- chaves de consent em src/lib/campaignConsentKeys.ts (client-safe); campaignInvite/campaignConsent re-exportam; onda0ConsentTexts referencia as constantes
- 'server-only' em 21 loaders Payload/cache (CLI segue com --conditions=react-server)
- ciclo de tipos supporterListFilters <-> supporterUi quebrado (SupporterListFilterInput estrutural; sqlFilters idem)
- votePledgeData dividido: votePledgeViews (puro, client-safe) + votePledgeData (loaders, server-only) - padrao contract-module
- predicados de role client-safe em src/lib/campaignRoles.ts; access/shared deriva deles (fonte unica); nav.ts para de importar o barrel campaignAccess (casts 'as CampaignUser' eliminados)
2026-07-25 14:38:41 -03:00
Francisco Ignácio Fontoura Solla
e2724c10da Remodel the campaign vertical from Praça to Município
Rename the operational unit "Praça"/"Núcleo" to "Município" (code identifier
`municipality`) and land the 2026-07-23 course changes:

- New `candidate` role with unrestricted visibility (`isCampaignStaff` now
  includes it) alongside coordinator/advisor; leader is locked down to the
  supporter contact tool only.
- Demandas become staff-only; dobradinhas modeled via a `stateDeputy` entity.
- Camaçari consolidated to a single município (ZE 170/171 aggregated);
  Salvador keeps its 19 zone municípios.
- Map moves to the staff dashboard; `/campanha/municipios` is list/overview.
- Vote pledges keep the declared vs estimated (scenario) asymmetry; leaders
  never see estimates.

Destructive migration for the campaign vertical (drops nucleus/plaza-era
tables, wipes campaign rows, seeds 435 municípios). Public-site data untouched.

Includes the post-delivery /simplify cleanups: shared reloadStaffActor /
reloadCoordinatorActor helpers, req.context cache for getFreshCampaignUser,
parallelized dashboard loads, candidate dashboard-copy fix, and dead
leader-dashboard / roleKind code removal.
2026-07-23 21:36:20 -03:00
Francisco Ignácio Fontoura Solla
cf13c441aa Remodel /campanha around predefined Praças replacing electoral nuclei.
436 seeded Praças (municipality-based; per-TSE-zone in Salvador/Camaçari) become
the operational unit; roles renamed to coordinator/advisor/leader with advisors
scoped to administered Praças. Leadership is one record per person across many
Praças/organizations; vote pledges split leader-declared vs staff-estimated
(estimates never serialized to leaders). Adds organizations, demand workflow
with staff-only cost/receipts, action-plan results with deputy presence, manual
political trend, multi-candidate comparison and the diverging red/white/blue
map mode. Migration 20260721_020109_remodel_plazas deliberately resets the
campaign vertical in production (public site untouched); test suite ported to
the new model (unit 187 / int 306 / e2e 9 green).
2026-07-21 02:28:00 -03:00
Francisco Ignácio Fontoura Solla
3b463173da Add campaign password reset, profile photo, and RS+ follow-up plan.
Deliver self-service forgot/reset via Resend, logged-in password change, and optional avatar on /campanha/perfil; register post-simplify engineering debts as RS+.
2026-07-19 05:26:56 -03:00
Francisco Ignácio Fontoura Solla
bcac665cb5 Ship /campanha nuclei MVP with RBAC, invites, and vote estimates.
Adds electoral nuclei, leaderships, updates, and WhatsApp invite flows behind campaign auth, plus the consolidated schema migration that Vercel applies on deploy.
2026-07-18 15:01:56 -03:00
Francisco Ignácio Fontoura Solla
c4866fabc0 Add /campanha internal area with isolated Payload auth
Introduce the internal campaign-management area gated by its own
authentication, kept isolated from the Payload admin so a campaign
session and an admin session can coexist in one browser.

- Add separate `campaignUser` auth collection (not `users` + roles)
- Authenticate via an isolated httpOnly `campaign-token` cookie scoped
  to /campanha, distinct from the default `payload-token`
- Add loginCampaign/logoutCampaign server actions and getCampaignUser
  session verification helper
- Add public login route and a protected (app) route group whose
  layout enforces the auth barrier
- Add migration 20260716_010420_add_campaign_user and document the
  auth foundation in AGENTS.md
2026-07-15 22:33:26 -03:00