Commit graph

324 commits

Author SHA1 Message Date
Francisco Solla
621364f41f feat(C139): página /campanha/contatos — lista desktop com células editáveis, criação e filtros 2026-08-13 14:29:11 -03:00
Francisco Solla
e71b062650 feat(C139): schemas e actions de contato — criação staff e edição por célula
Fase 3 (server) do plano aprovado:
- schemas/contact.ts: contactCreateSchema (nome único via invariante, state
  obrigatório, phones opcionais C112); contactFieldUpdateSchema ganha variantes
  city e postalCode ('' → clear); mensagens das actions client-safe aqui.
- schemas/personGenders.ts: enum de gênero em módulo folha (ciclo de imports
  contact ↔ leadership detectado e quebrado).
- actions/contact.ts: createContact (gate staff + bypass justificado, lock
  contact-name:{normalized}) e updateContactField (escopo 'edita o que vê'
  via re-check user-threaded para advisor; reorder de telefone primário).
  Apagar reusa as actions C100 existentes.
- collections Contact e Supporter: label de gênero 'Outro' → 'Não binário'
  (sem migration; nenhum teste pina a label).
- 11 testes unit novos; lint, tsc e prettier limpos.
2026-08-13 14:12:53 -03:00
Francisco Solla
1daaaf9f4e feat(C139): domínio de contatos — URL state, loader com vínculos e invariante de nome
Fase 2 do plano aprovado. Três módulos em src/utilities/contacts/:
- contactListUrl: contrato de params (q|gender|state|city|ausencia|vinculo|sort|dir|page),
  canonicalize, hrefs de sort e where de fonte com 'phones.value' like (ILIKE verificado
  no Postgres local).
- contactListData: loader com UM find em contact (user threadado, overrideAccess false),
  vínculos (liderancas/dobradinhas/assessores/equipe) como facets sobre o conjunto
  legível, filtro/sort/paginação em memória, nulos no fim (precedente B15).
- contactNameInvariant: lock contact-name:{normalized} + prefilter ILIKE + veredito
  exato em memória; mensagem de conflito pt-BR.

38 testes unit novos; format + tsc limpos.
2026-08-13 14:05:24 -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
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
7629a505e0 feat(C115): edição pelo Google Calendar volta para o Teqo (motor bidirecional + webhook push)
- runSyncPass bidirecional: regra do relógio decide direção (Google mais novo
  aplica título/horário; Teqo re-asserta), cancelamento via showDeleted +
  snapshot lastSeenEventIds, registro no updates feed
- webhook público /campanha/agenda/google-webhook/[secret] fail-closed
  (secret + X-Goog headers constant-time), canal lazy sem cron (ensure em
  toda passada, renovação com id novo + stop do antigo)
- migration hand-written (campos de canal/snapshot em googleCalendarSync);
  mutationKind dedicado libera título via Google sem quebrar slug canônico
- UI: seção 'Edições pelo Google' no dialog + autor 'Google Calendar' no feed
- testes: unit (reverse mapping, validação webhook), int (motor bidirecional
  com stub, 10 cenários), e2e (webhook 404 fail-closed)
2026-08-11 22:38:53 -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
7fabb72df8 feat(C125): facet de ausência completo (qualquer_ausencia) e descoberta de ordenação no mobile
- PEOPLE_ABSENCES ganha o 4º valor qualquer_ausencia ("Qualquer ausência"),
  a união exata das três ausências em UM chip — o 3º clique dos específicos
  deixa de colapsar o facet (3 de 4 membros) e o parse absorve o guarda-chuva
  sobre os específicos, sem tocar no B18 genérico de capacity/status
- peopleListSortPrimaryOptions: seeds do grupo Ordenação viram as 7 direções
  primárias com emptyQueryVisible, então o mobile descobre as 7 chaves ao
  tocar no bar (e "ordenar" não corta aliada/assessorado/base no cap 8)
- CampaignListOmnibox reabre o popover no mousedown: clicar num input já
  focado (logo após commitar busca com Enter) não disparava focus, deixando
  a descoberta morta no mobile até digitar
- tests: contrato de URL (absorção + não-colapso), predicado + AND, novo spec
  de omnibox, e2e do 3º clique e da descoberta mobile (390×844)
2026-08-11 20:34:51 -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
94c89a57a1 feat(C124): tarefa de atividade só com texto, responsável e concluída (sem prazo) 2026-08-11 16:05:07 -03:00
Francisco Solla
a8414a49f1 Merge origin/main into C117 (C116/C119/OPS40) — sortable heads sobre células editáveis 2026-08-11 15:43:23 -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
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
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
b42de3e7ec feat(C117): ordenação por coluna (header + omnibox) e filtros de ausência em Pessoas
- sort/dir/ausencia no contrato de URL de /campanha/pessoas (molde territórios/B15)
- sortPeopleRows sobre o conjunto filtrado, nulls no fim, tiebreak nome→contactID
- PeopleSortableHead (CampaignSortableHead) com aria-sort nas 7 colunas visíveis
- omnibox: grupo Ordenação (chave×direção) e facet Ausência (Sem assessor/base/contato)
- clear preserva o sort; tests unit + e2e; sem migration/collection
2026-08-11 13:33:21 -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
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
170ce59ef4
Merge pull request #640 from fsolla/C111-telefone-nao-unico-duas-pessoas-podem-compartilhar-o-me
C111 — Telefone não único: duas pessoas podem compartilhar o mesmo celular
2026-08-11 03:58:55 +00: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
419ba39ecc
Merge pull request #639 from fsolla/B197-ocultar-a-coluna-de-e-mail-por-padrao-nas-listas-de-pes
B197 — Ocultar a coluna de e-mail por padrão nas listas de pessoas
2026-08-11 01:39:57 +00:00
Francisco Solla
3e9390230b feat(B197): e-mail oculto por padrão nas listas de pessoas — default B17 nas 5 listas + coluna própria em pessoas/apoiadores + picker em assessores 2026-08-10 21:57:52 -03:00
Francisco Solla
c4212b3e03 feat(push): opt-in automático e fix do prompt nativo no /campanha
- requestPermission agora é chamado sincronamente no clique: dentro do
  startTransition(async ...) o navegador perdia a user activation e o Chrome
  auto-negava silenciosamente ("Permissão de notificação negada" sem prompt)
- CampaignPushOptInToast no shell (app): staff com permission 'default', sem
  inscrição ativa, consentimento publicado e VAPID configurado; dismiss em
  localStorage; iOS só com app instalado
- cerimônia de ativação/desativação extraída para
  src/utilities/campaignPushClient.ts, compartilhada com o card de perfil
- mensagens distintas para denied (reativar nas configurações do site) vs
  default (prompt não exibido); pin de top-level registrado
- CHANGELOG-AGENTS.md: entrada única
2026-08-10 21:14:56 -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
4baa617c01
Merge pull request #610 from fsolla/C110-agenda-mobile-feedback-visual-do-arrasto-reveal-do-peri
C110 — feedback visual do arrasto na agenda mobile (reveal do período adjacente + commit/snap-back)
2026-08-10 15:02:53 -03:00
Francisco Solla
3b81431d39 refactor(C110): simplify — helpers de dominância/clamp, seed e2e generalizado, CSS sem regras mortas 2026-08-10 14:42:54 -03:00
Francisco Solla
b23a323f83 feat(C110): feedback visual do arrasto na agenda mobile — reveal do período adjacente + commit/snap-back 2026-08-10 13:51:55 -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
b210939226 refactor(B193): simplify fixes pós-revisão — mapper único de updates, API de trigger uniforme, grupos de relação extraídos, hardening do sheet host
- Extrai toMunicipalityUpdateViewModel/loadUpdateAuthorNames compartilhados
  (feed + batch da lista); remove avatarClassName morto.
- MunicipalityRelationTriggerProps compartilhado nos 3 wrappers; triggerClassName
  no ExpectedVotesControl; ChipLabel/RelationGroup/CityDash no card.
- Sheet host: cleanup de unmount no overlay (sem drawer órfão), registerFooter
  valida open, branch nulo do handleOpenChange limpa footer; chrome freeze
  documentado.
- A11y/UX: ícone de alerta no rodapé frio (WCAG 1.4.1), iniciais de avatar
  aria-hidden, chip sem_base sem target morto, label da cidade com Tendência,
  aria-label da barra anuncia os 3 cenários, card layout 'colocação · votos'.
- E2E: settleStream no B184 (flake da cópia transitória do streaming), cards
  ancorados por hasText (q= casa prefixos), cor da borda via classe do token.
2026-08-10 11:23:49 -03:00
Francisco Solla
3d8dbae61f feat(B193): card de município mobile denso com edit-where-you-see e rodapé de atualização expandível
- Card denso (wireframe): posição de voto à direita, barra de cenários com
  marcador do ativo, chips classe/tendência/nível (label à esquerda), 3 grupos
  de avatares com wrap (um por entidade), rodapé Última atualização + chevron
  que expande revelando o último card de atualização + CTA registrar; sem
  atualização → CTA direto; prioridade = borda lateral direita 6px primary.
- Controles ganham prop trigger/triggerClassName (precedente RelationChipCell);
  MunicipalityRelationAvatarStack ganha modo wrap.
- C109 (sheet quebra ao abrir) corrigida: sessões do sheet host keyed por id
  (imune à identidade dos callbacks recriados por render) e footer customizado
  renderizado pelo host dentro da árvore do Drawer (portal perdia o Dialog
  context — reproduzido digitando no sheet).
- Dado: MunicipalityListViewModel.lastUpdate (batch por página, scoped).
- E2E novos (barra/votos, chip nível, borda prioridade, expansão/CTA vazio).
Sem migration/access/Consent.
2026-08-10 11:23:49 -03:00
Francisco Solla
bc661821c8
Merge pull request #574 from fsolla/B192-sollinha-follow-ups-sugeridos-apos-cada-resposta-instru
B192 — Sollinha: follow-ups sugeridos após cada resposta (instruídos no prompt, extraídos pela UI)
2026-08-10 09:37:45 -03:00
Francisco Solla
382c4c46ec feat(B192): follow-ups sugeridos após cada resposta do Sollinha (bloco no prompt + chips extraídos) 2026-08-10 09:13:38 -03:00
Francisco Solla
b354ad55d6 style(B186): simplify fixes — NaN tiebreak, janela simétrica, testes sem bomb-relógio
- byGravity estagnação: Infinity - Infinity (NaN) matava o tiebreak para dois
  municípios nunca-sinal (topo da lista) — comparação explícita + teste
- fronteira da janela: módulo puro inclusivo (>= cutoff) e query da tool em
  greater_than_equal (antes greater_than, assimétrico) + testes 30/31 dias
- ultimoSinalAtrasDias reporta a idade da atualização decisiva no balde sinal
  (E9 podia devolver null ao lado de evidência 'há 2 dias' em linha stale)
- fixtures da tool com datas relativas a Date.now() (absolutas apodreceriam
  em ~30 dias); assert de user: ctx.user no caminho do assessor
- options type em inglês (windowDays/reason/sortBy); MunicipalityPriorityReason
  não exportado (knip); agora passado da tool (um relógio)
- expectedVotes select estreito { central: true }; sort '-createdAt' removido
  (módulo re-deriva o decisivo); NARROW_SCOPE_HINT compartilhado em aiToolScope
- baseResponse unifica o shape (short-circuit + retorno); docblock órfão da
  extração removido; prompt: coordenação/candidatura
- impl plan: registro de execução + shape entregue
2026-08-10 08:37:49 -03:00
Francisco Solla
11886a653d feat(B186): Sollinha — prioridades do momento (getMunicipalityPriorities)
Tool nova do chat que responde "quais devem ser minhas prioridades neste
momento?" com top N municípios do escopo, cada um com uma linha de
evidência e o critério declarado. Ranking derivado na hora em módulo puro
(municipalityPriorities.ts): 3 baldes por gravidade (sinal desfavorável
recente > estagnação > potencial alto com nível baixo), exclusão de
atualização recente favorável, frescor E9 (max lastUpdateAt/lastPledgeAt),
potencial = expectedVotes.central com fallback nos válidos 2022 (artefato).

Resolver de escopo da B185 extraído para aiToolScope.ts (2º consumidor;
getPendingLeaderships refatorado sem mudança de comportamento). Gate staff
fail-closed, leituras overrideAccess: false + user. Seção nova no system
prompt. 38 unit tests novos; 1772 unit + 680 int verdes; build OK.
2026-08-10 08:37:49 -03:00
Francisco Solla
2b3c48e063 refactor(B189): simplify — nomes alinhados à B186, loader de nomes compartilhado, guard de escopo vazio
- aiToolScope.ts: AIToolScope/AIToolScopeKind/AIToolScopeMunicipality
  (mesmos nomes da extração da B186 em voo — conflito de merge vira trivial)
- loadNamesByIds da B185 e loadAdvisorNames da B189 fundidos em
  aiToolQueries.ts (loadAIToolNamesByIds) — don't twin
- guard fail-closed de escopo vazio (assessor fora do portfólio) + teste
- sortStale sem ?? 0 (partição garante ISO) — type honesty
- prompt: exemplo de Salvador com limiar estrito; index ordenado
2026-08-10 08:12:00 -03:00
Francisco Solla
6027aac941 feat(B189): Sollinha — municípios sem atualização recente (tool getMunicipalitiesWithoutUpdate)
Tool nova da família B185/B186: cobertura do acompanhamento por recência
de municipality.lastUpdateAt (derivado dos hooks de municipalityUpdate),
limiar default 30 ajustável, lista exaustiva com total, nunca atualizados
no topo, assessores por item. Resolver de escopo extraído para
aiToolScope.ts (2o call site; B186 sera o 3o). Gate staff fail-closed;
unit tests 14 novos + regressao B185 verde.
2026-08-10 08:12:00 -03:00
Francisco Solla
4373599192 feat(B190): Sollinha — cobertura de parcerias (municípios sem dobradinha + dobradinhas órfãs)
Nova tool getPartnershipCoverage com dois modos: municípios do escopo sem
nenhuma dobradinha vinculada (Salvador agrupada por cidade, drill-down por
ZE) e dobradinhas órfãs (sem município vinculado no registro inteiro, via
bypass agregado-only do helper B34). Gate fail-closed para leader; região
tolerante a acento; critério declarado na resposta. buildCampaignLinks ganha
o filtro stateDeputies sem_dobradinha na lista de municípios (URL B176).

Closes #527
2026-08-10 07:24:15 -03:00
Francisco Solla
7cf820f51b fix(B185): revisão /simplify — escopo efetivo de assessor, chips sem vazamento, DRY e convenções
- Município/território real fora do portfólio devolve lista vazia escopada (antes
  erro 'não reconhecido') + escopoRestrito: true declarado na resposta
- Chips de município fora do acesso omitidos (nunca id pelado)
- uniqueRelationshipIds canônico no lugar do uniqueIDs local
- Chaves do input zod em inglês (scope/filter/mode); Salvador sem round-trip
- Pledge query restrita a lideranças engajado + dica quando truncado + prompt
2026-08-10 03:07:13 -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
03a2f57d03 feat(B185): tool do Sollinha para lideranças pendentes de abordagem + assessores responsáveis
getPendingLeaderships: escopo por território/cidade(Salvador=19ZE)/município com
folding de acento, critério de pendência declarado (status A abordar/Em disputa
ou Engajado sem votePledge no escopo; negativo excluído na query), filtro
sem_assessor (C3), modo municipios_sem_lideranca (C4), nomes de assessores via
query própria, gate staff fail-closed (leader deny antes de qualquer query).
Sem migration/Consent/UI; unit tests 14 novos; allowlist P3-D justificada.
2026-08-10 02:27:04 -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
cd41b44de4 feat(C105): tags no sheet da agenda + rótulo no evento + prefill no "Mais detalhes"
O filtro por tag na omnibox já existia (C94); o C105 completa a superfície da
agenda: campo de tags no overlay de criação inline (chips, sugestões das tags
já usadas), rótulo leve #tag no evento do calendário (oculto no mês/mobile) e
tags via prefill do "Mais detalhes" (params repetidos, parse fail-closed).

- ActivityTagInput extraído do ActivityForm (compartilhado, aria-label novo)
- "Mais detalhes" espelha estado vivo (horários editados + tags)
- popover do sheet com max-h --radix-popper-available-height (shift-before-flip
  do Radix clipava os campos do topo em viewports baixos)
- submit lê tags do estado espelho (fonte única), 4 KB cap p/ responsáveisJson
- limites mantidos 20/80 (C14), sem migration/access/Consent

Impl: docs/plans/agenda-tags-eventos-impl.md (aprovado no gate humano)
2026-08-10 00:21:20 -03:00
Francisco Solla
a7dce2c716
Merge pull request #516 from fsolla/C101-agenda-mobile-calendario-com-cara-de-app-nativo-context
C101 — Agenda mobile: calendário com cara de app nativo (contexto no header, navegação por arrasto, filtro sticky, dia na hora atual)
2026-08-09 22:43:12 -03:00