Commit graph

218 commits

Author SHA1 Message Date
Francisco Solla
807d937e92 fix(test): DEBT-FLAKES-C100 — silence review sample 10→30 + ensureLeasedConsent ON CONFLICT idempotent (Closes #553) 2026-08-24 17:37:14 -03:00
Francisco Solla
4034fa371e fix(E16): reconcile allocation_decision.rationale NOT NULL drift
Drop the NOT NULL constraint on allocation_decision.rationale to align
the DB schema with the Payload config (required: false since B134).
Hand-written idempotent migration following the consent_text_to_jsonb
precedent. Adds idempotency test in campaignMigrationReconciliation.

Closes #371
2026-08-24 17:24:13 -03:00
Francisco Solla
5b4e5ecf22 ops(OOPS80): cutoff fase 1 — desativar acesso pela URL antiga pt.jorgesolla.com.br (congelar a fonte)
- scripts/check-push-chain.mjs: default --site, usage, erro → canônico; prosa Neon/vercel → homeserver
- tests: fixtures unit (WebAuthn, JsonMutation, aiMarkdownLinks) + int (campaignMultiPhones) → canônico, semântica preservada
- campaignMultiPhones: adiciona ensureLeasedConsent para invite consent fixture (fix pre-existente)
- personas (.opencode/agent/designer, solla-comunicacao, skill): canônico + operação homeserver
- docs: intenção + impl plan + changelog entry; CHANGELOG-AGENTS append-only preservado
- Sem migration, sem schema, sem access/Consent, sem UI
- Closes #796
2026-08-23 20:14:59 -03:00
Francisco Solla
368f138282 feat(campaign): C88 deliberação na atualização — responsável, fio e resolvido (Closes #397)
Migration 20260823_161905_add_municipality_update_deliberation + access por mutationKind + ilha client nas 3 superfícies + specs unit/int/e2e.
2026-08-23 18:58:26 -03:00
Francisco Solla
a74a2dca9e refactor(OPS46): simplify pós-revisão — release do claim ao fim do cleanup (janela purge segura), gate da hook exige o par de flags, Set→boolean, guard setval/nextval\s*\( e spec de steal invariante-primeiro 2026-08-23 14:24:33 -03:00
Francisco Solla
89bf9d0854 ops(OPS46): e2e local coleta via hook ESM (next/cache + server-only) e allocator de município com registry anti-wrap (Closes #733)
Fase 1: playwright bare (--list, extensão VS Code, runs ad-hoc) quebrava na
coleta — next@15 sem exports não resolve 'next/cache' em ESM nativo e
server-only exige a condição react-server; o script test:e2e carregava ambas
via NODE_OPTIONS (por isso o CI passava). Hook de resolução ESM vitest-free
(e2eEsmResolve.mjs) registrada no main via module.register + propagada aos
workers via NODE_OPTIONS quando invocada bare; path do script intocado.

Fase 2: a sequência persistente campaign_fixture_municipality_alloc nunca é
resetada e um ciclo completo claima ~505 do catálogo de 435 — o wrap em todo
ciclo em DB de worktree fazia dois runs concorrentes receberem o mesmo índice
e purgarem as linhas vivas um do outro. Registry de claims compartilhado
vitest-free (campaignMunicipalityAllocator.ts): INSERT ON CONFLICT DO NOTHING
com skip de slot vivo, steal de claim stale (TTL 2h), release no cleanup dos
dois fixtures, erro claro de exaustão. Guard estático no
testMunicipalityAllocatorConventions + spec int de stress.
2026-08-23 14:02:19 -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
2cc8f2c14d feat(C143): demandas visíveis apenas para responsáveis explícitos + candidato/coordenador
- campaignDemand.responsibles (hasMany → campaignUser, staff elegível) com
  hook beforeValidate que descarta ids não-staff (fail-closed)
- canRead/UpdateCampaignDemand por membership: assessor só vê demandas onde
  é responsável; unrestricted/admin sempre; leader nunca (fim do escopo
  municipal de demandas)
- criador entra automaticamente como responsável no hook de criação (form,
  wizard, rascunhos C90); migration com backfill created_by_id → responsável
- UI: picker multi-select na criação (nova + wizard) e card de gestão no
  detalhe com save full-replace + advisory lock; sugestões = assessores do
  município; rótulo 'Visível para: Responsáveis · Candidato · Coordenador'
- int tests reescritos para a regra nova + 5 casos novos; e2e novo de
  visibilidade (lista/URL/gestão); fixture e2e com createCampaignDemand
- impl plan + changelog; importMap regenerado com S3_* dummy (OPS69)
2026-08-19 23:55:07 -03:00
Francisco Solla
398a91e54a feat(S9): captura de novidades da campanha na home pública (form + CTA do hero)
Seção 'Receba as novidades da campanha' acima do rodapé: nome + WhatsApp
obrigatórios, e-mail/estado/cidade/comentário opcionais, toggle pré-selecionado
'Quero fazer parte do time' gravado como subscription.campaignLevel
(time|esporadico). CTA secundário do hero vira o atalho (#novidades, scroll
suave). Contact + Subscription em withPayloadTransaction com consentimento
novo fail-closed por chave estável 'campanha-novidades' — nada é gravado sem o
texto aprovado no admin. Migration 20260819_192605 (state de contact nullable
+ coluna campaign_level). Impl plan, changelog, int + e2e.
2026-08-20 02:19:58 +00:00
Francisco Solla
bf25100ed5 chore(OPS45): e2e fixtures — purge de resíduo no claim de município + robustez dos int specs (Closes #723)
O cleanup do campaignE2EFixtures estava completo em runs normais (proxy auto-own
cobre contact/stateDeputy); o lixo vinha de runs crashados, cujo cleanup nunca
roda. O claim e2e agora purga resíduo como o int já fazia, e a purga ganhou a
classe dobradinha (stateDeputy vinculados via municipality.stateDeputies + fichas
órfãs com ref-check incluindo stateDeputy — FK ON DELETE RESTRICT).

peopleList.int.spec.ts passa a varrer todas as páginas (lista global de 25/pág)
com dedupe; personDelete/peopleList usam personName() nos nomes fixos, imunes ao
invariante global de nome de dobradinha.
2026-08-13 14:18:54 -03:00
Francisco Solla
65659bac2f C130: ajustes da tabela desktop de pessoas (contato completo, base sob o nome, caret, tooltips, Dobra em, filtro/ordenação por partido) 2026-08-12 16:01:25 -03:00
Francisco Solla
5e797d8641 refactor(C128): fixes do /simplify — locks por pessoa no exit destrutivo, undo-toast suprimido, guard anti-race
- Assessora/StateDeputy: advisory lock por pessoa (person-assessora /
  person-state-deputy) serializa enumerate→decide→write do exit destrutivo
  (dois removes concorrentes de chips distintos deixavam zumbi com carteira
  vazia — P1 do reviewer A); enumeração da carteira usa getAdvisorMunicipalityIds
  canônico (F1).
- RelationChipCell: commitGuard retorna boolean | 'destructive'; o veredicto
  destrutivo suprime o toast 'Desfazer' do lote (re-add só re-criaria a
  entidade vazia — votos/conta já foram) — P2 A+B.
- PeopleMunicipalityCell: sentinela manifestPending (dois guards concorrentes
  sobrescreviam o resolver e a 1ª remoção morria em silêncio — P2-1) + cleanup
  no unmount resolve false (P2-2) + extraFormFields memoizado; minItems órfã
  removida.
- personDelete: bloco batch redundante de autorias removido (o loop com
  deleteCampaignUserAccount já cobria — 4N DELETEs no-op — F4).
- getPersonCapacityExitManifestAction: actor reloaded (papel rebaixado não lê
  manifest que o write recusaria — P2-A).
- Revalidação: setPersonStateDeputyMunicipalities ganha
  revalidateMunicipalityListPaths (paridade com o twin B37 — F2).
- Testes: int +2 (cascata de autorias no exit da Assessora; undo re-cria a
  liderança)
2026-08-12 15:29:56 -03:00
Francisco Solla
c0645067f1 feat(C128): ciclo de vida das capacidades pelas células de /campanha/pessoas (Closes #695)
Qualquer pessoa visível ganha/perde Assessora, Lidera e Aliada em pela
lista: a primeira cidade cria a entidade (conta staff sem credenciais
utilizáveis / liderança / dobradinha), a última remove da tabela com
confirmação destrutiva (manifest: votos+convites da liderança, conta+
autorias+assessorado da assessora; dobradinha sem diálogo). Saída
destrutiva por escopo: assessor encerra Lidera/Aliada com a capacidade
inteira na carteira; Assessora segue coordenação/candidato. Escritas
person-centric por contactId com extração dos deltas B34/B37 e da
cascata de conta (deleteCampaignUserAccount, nextFreeStubCampaignEmail,
matchers de conflito) — sem twin. commitGuard/commitWithNullOwner/
extraFormFields como opt-ins no RelationChipCell. Corrige bug latente
C116 (célula Assessora nunca salvava — contactId faltava no FormData).
Sem migration. Int: 28, unit: 6, e2e: +2.
2026-08-12 15:29:56 -03:00
Francisco Solla
184f0ca128 test(C123): int spec do edit draft usa startAt fora da janela do espelho Google (C126) 2026-08-12 14:16:20 -03:00
Francisco Solla
db8102b47e feat(C123): agenda — overlay modal central como única superfície de criação/edição de atividade (Closes #667) 2026-08-12 14:16:20 -03:00
Francisco Solla
3593d84b30 refactor(C129): simplify — CampaignNameSubline compartilhado (a11y sr-label), guard legenda==nome, sem plumb morto, spec mais forte 2026-08-12 12:52:35 -03:00
Francisco Solla
d7acb5aa48 feat(C129): nome de legenda da dobradinha — campo ballotName + display discreto sob o nome (Closes #696) 2026-08-12 12:52:35 -03:00
Francisco Solla
9d330f48e4 merge(C115): rebase em main (C126 activityWhere, C124 tasks, C122 e2e estados, OPS40/41) — spec portado para runSync escopado 2026-08-11 22:48:14 -03:00
Francisco Solla
2f247d40a6 fix(C115): revisão /simplify — relógio com baseline de espelho, reopen sem re-cancelamento, hardening do webhook
- lastMirroredChangeAt (activity, migration): clock rule compara contra a
  última mudança espelhada, não updatedAt (task toggle não silencia edição
  pendente do Google)
- snapshot exclui ids que o motor deletou (removedEventIds separado do
  remoteEventIds): reopen pós-cancelamento não vira re-cancelamento fantasma
- cancelamento do Google respeita o relógio: cancel mais novo cancela; Teqo
  mais novo (reopen) limpa o trashed e recria
- escrita reversa sem req.user: autoria do registro correta + sem gate 403
  do deputado; hook de sync pula a própria escrita reversa (sem passada aninhada)
- canal: segredo rotaciona por criação; novo canal persistido antes do stop
  do antigo; identity fields read:false; create admin-only; leitura prefere
  linha configurada; pre-filter do webhook antes do bootstrap do Payload
- testes: all-day ida-e-volta, task-toggle, reopen, slug congelado, e2e dos
  caminhos 200, render tests do dialog/feed; asserções herméticas a
  atividades ambiente dos arquivos irmãos (DB compartilhado)
2026-08-11 22:42:05 -03:00
Francisco Solla
c69990a3f0 fix(C126): isola o escopo do espelho Google nos int specs (fim do flake paralelo) 2026-08-11 21:33:07 -03:00
Francisco Solla
6d41517a05
Merge pull request #705 from fsolla/C122-c114-follow-up-cobertura-de-testes-dos-estados-do-espel
C122 — cobertura de testes dos estados do espelho Google (componente + e2e + action)
2026-08-11 23:49:39 +00:00
Francisco Solla
c7c9556ff3 test(C122): cobertura dos estados do espelho Google — unit do chrome, e2e dos 4 estados + FAB, int da action de desativar 2026-08-11 19:43:19 -03:00
Francisco Solla
c0ffc22026 feat(C118): página de detalhe da pessoa — seções montadas pelas capacidades (Closes #657) 2026-08-11 16:32:21 -03:00
Francisco Solla
f1405b04a5
Merge pull request #680 from fsolla/C119-status-de-apoio-lembranca-em-liderancas
C119 — Status de apoio 'Lembrança' em lideranças
2026-08-11 18:25:18 +00:00
Francisco Solla
04d9940317 fix(C116): editor de Contato convive com o shape phones do C112 (primary phone via reorderWithPrimaryPhone) 2026-08-11 15:09:56 -03:00
Francisco Solla
a83bedd7a8 feat(C116): pessoas — edição onde você vê na lista desktop (células sempre input, chips territoriais internos, nome-link, tooltips nas ações) 2026-08-11 15:09:56 -03:00
Francisco Solla
3a4e44f422 chore(C119): regenera migration sobre main rebaseada (C112 phones) e ajusta teste int para schema phones 2026-08-11 14:52:49 -03:00
Francisco Solla
5fa4e22c9d feat(C119): status de apoio 'Lembrança' em lideranças — enum, badge e exclusão de pendências 2026-08-11 14:52:49 -03:00
Francisco Solla
70070db8dc feat(C114): agenda da campanha espelhada no Google Calendar com notificação (push Teqo → Google)
Espelho oficial da agenda em calendário Google compartilhado via API
(service account, jose + fetch): reconciliação por passada com id
determinístico teqo+base32hex (upsert idempotente, sem estado por evento),
hooks afterChange/afterDelete na activity (só campos espelhados, nunca
lança), auto-resync quando calendarId muda, retry na view da agenda,
pill de estado no header + FAB + diálogo (4 estados). Cancelado no Teqo
deleta o evento no Google. Sem participantes no v1 (decisão de produto).
Sem migration nova de tabela (hand-written). Closes #635
2026-08-11 13:53:51 -03:00
Francisco Solla
c83bd779d2
Merge pull request #665 from fsolla/C112-multiplos-telefones-por-pessoa
C112 — Múltiplos telefones por pessoa
2026-08-11 14:27:47 +00: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
c81a439d36 merge: origin/main (C113/OPS38 changelog, wizard chrome) 2026-08-11 09:01:13 -03:00
Francisco Solla
5577d31c38 fix(C113): feed iCal da agenda vivo — contrato de frescor no lugar do snapshot congelado
Spike no build de produção provou que o Next 15 não congela route handlers GET
(rota dinâmica por default); o freeze era o contrato de resposta
(public, max-age=3600 sem validadores) permitindo caches compartilhados
reterem o snapshot do primeiro fetch. Fix: force-dynamic explícito +
buildICalFeedResponse com public no-cache, ETag forte sha256, Last-Modified,
304 só por If-None-Match (nunca por data — blind spot de exclusão), dica
X-PUBLISHED-TTL no corpo do VCALENDAR (RFC 7986) e 404 no-store para
revogação fail-closed no CDN. Testes unit/int/e2e (dev + prod build).
2026-08-11 08:42:42 -03:00
Francisco Solla
8d007285d5 chore(D10): auto-cura de leftovers de consent/supporter + janela missing-consent exclusiva (12/12 sob 6 workers)
F1: purgeOrphanedConsentRenames sob lease exclusivo (autocommit) nos 3
pontos que tocam a key canônica; renames de teste migrados para chaves
privadas; F1b: purge de contatos órfãos no purgeMunicipalityResidue.
F2 (medição): shared-writer ABBA refutado; janela do missing-consent
exclusiva + Onda0 atômico (down transacional) + provision exclusivo.
Contraprova: 7 arquivos da key x 6 workers = 12/12 verdes (baseline 1/4).
2026-08-11 07:41:04 -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
48707d0bff
Merge pull request #630 from fsolla/D9-d9-flake-do-consent-restore-do-testdatabaselease-sob-carg
fix(D9): flake do consent restore do testDatabaseLease sob carga — janelas leaseadas, Onda0 com restore de ids, asserções por snapshot
2026-08-10 23:45:52 +00:00
Francisco Solla
9e2b22ef86 fix(D9): flake do consent restore sob carga — janela leaseada, Onda0 shared com restore de ids, asserções por snapshot
- withMissingInviteConsentFixture serializa a janela de operação sob lease
  (serializeWindow default true; teste de ensures concorrentes migrou para
  key/lease privada; re-verificação/re-delete do gap setup→janela)
- spec Onda0 volta a leases shared (exclusivo causava ABBA com escritores de
  invite) e o down-migration restaura as linhas com os ids originais
  (restoreConsentRows compartilhado; no-op para o resto da suíte)
- asserções de restore referenciam o snapshot do fixture (contrato real);
  rollback asserta id+key; ORDER BY/sort nas leituras limit-1
- contraprova: suíte int completa ×5 + 6 arquivos da key ×13/14 verdes
2026-08-10 20:23:39 -03:00
Francisco Solla
712d40f83f feat(B195): finalizar wizard Registrar pedido — campo único + título derivado por IA + edição no detalhe 2026-08-10 20:15:05 -03:00
Francisco Solla
cc7dff88c4 feat(C100): lista unificada de pessoas em /campanha/pessoas
Merge por Contact (uma linha por pessoa física) de lideranças,
dobradinhas e staff, com capacidades como colunas; omnibox com
capacidade/município/apoio + filtros salvos (2º call site do B18 via
factory createSavedFilterStore); apagar pessoa com manifest de
confirmação e cascata transacional; mobile edge-to-edge (omnibox sem
bordas, cards com divisórias, salvar filtro no header). Sem migration.

Closes #495
2026-08-10 13:45:46 -03:00
Francisco Solla
c3f10cce23 feat(C108): sino de notificações — abrir marca tudo como lido, painel limpo, modal central no desktop
openCampaignNotifications (lista + marca em 1 round trip) substitui
listCampaignNotifications + markAllCampaignNotificationsRead; markAllNotificationsRead
extraída (owner-scoped, idempotente, cap 200) com int test; bell vira Dialog
desktop / Drawer mobile (par C94), sem título/rodapé/botões, skeleton de
loading, safe-area, focus-visible, role=alert; readAt sai do contrato do item;
P4-D ledger atualizado (batch sem transação agora no hot path, self-healing).
2026-08-10 02:56:20 -03:00
Francisco Solla
ea700e3113 feat(C104): compromisso 'Todo o dia' na criação rápida da agenda (só datas, multi-dia) 2026-08-10 01:13:12 -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
Francisco Solla
6c34da4a47
Merge pull request #488 from fsolla/B178-salvador-cidade-agregada-no-campanha-pagina-virtual-lin
fix(B178): review do simplify — ordenação de nulls nativos, mapa sem slug da cidade, a11y e testes de borda
2026-08-09 10:53:46 -03:00
Francisco Solla
d8ee2dbafa fix(B178): review do simplify — nulls nativos em DESC, mapa sem slug da cidade, sr-only morto, sinal mobile, paginação/edges testados 2026-08-09 10:40:18 -03:00
Francisco Solla
f1a8332835
Merge pull request #487 from fsolla/B178-salvador-cidade-agregada-no-campanha-pagina-virtual-lin
feat(B178): Salvador como cidade no /campanha — página virtual, linha comum na lista e busca, sem dupla contagem
2026-08-09 10:21:16 -03:00
Francisco Solla
de66fe3e3f feat(B178): Salvador como cidade no /campanha — página virtual, linha comum na lista e busca, sem dupla contagem 2026-08-09 10:07:05 -03:00
Francisco Solla
2ba7a88531 feat(C93): allow generating calendar import link without filters
The 'Link de import' button was disabled whenever no agenda filter was
active, forcing a filter to subscribe to the full campaign agenda. The
server already supported the empty-filter case (filterMunicipality empty
= entire creator scope via buildFeedWhere/resolveFeedCreatorAccess), so
this unlocks the button and drops the now-dead state/hasFilters props.
The dialog copy is neutralized ('recorte' -> 'agenda') to cover both the
filtered and filterless shapes.

Int tests pin the content acceptance: coordinator filterless feed covers
all municipalities; advisor filterless feed is scoped to the administered
portfolio (nothing out-of-scope leaks); an advisor with no assigned
municipality serves an empty feed (fail-closed). A new component test
asserts the button is enabled with zero filters and the create flow works.

Closes #437
2026-08-09 09:02:06 -03:00
Francisco Solla
663c57919f fix(B176): ordena o scope esperado do int spec (alocação não é ascendente) 2026-08-09 03:19:54 -03:00
Francisco Solla
14bbee6e00 feat(B176): filtrar municípios por Dobradinha/Liderança/Partido + coluna Dobradinhas ao assessor
Omnibox de /campanha/municipios ganha as dimensões Dobradinha (nome do
contato), Liderança (nome do contato) e Partido (sigla), com recortes
"Sem dobradinha"/"Sem liderança"/"Sem partido" no precedente de
"Sem nível". Params aditivos stateDeputy/leadership/party na URL
congelada; buildMunicipalityListWhere ganha catálogo request-scoped
fail-closed (municipalityRelationSets) que vira as leituras reversa e
cruzada em where — sem migration, paginação nativa mantida; facets
estendidas e o mapa compartilha o catálogo. Coluna Dobradinhas liberada
ao staff (servidor já scoped B37/B157; só o gate de UI mudou).
Extrato do par sentinela+exists:false em campaignListUrl (5 call sites).
2026-08-09 03:19:54 -03:00
Francisco Solla
5a7f3bbb8a
Merge pull request #475 from fsolla/B177-sollinha-responder-em-quais-cidades-x-foi-o-deputado-ma
feat(B177): Sollinha responde em quais cidades X foi o deputado mais votado (rank competitivo reverso, flexível)
2026-08-09 02:46:17 -03:00