O renderer de links do chat decide interno vs externo (B187/B188); a decisão
pura de fechamento mora no owner src/lib/ai/markdownLinks.ts
(shouldCloseDrawerOnLinkClick: interno && mobile && clique simples) e o
markdownComponents estatico virou useMemo no corpo do CampaignAIChat para
alcancar o contexto. Desktop inalterado; links externos/modos de nova aba
mantem o drawer da aba de origem; sessao e restore intactos (close persiste
open:false).
- unit: 12 testes da decisao pura
- e2e (modo CI, E2E_PROD=1): interno fecha + navega sem load + reload nao
reabre; externo abre popup e mantem o drawer
- debito B199/#672 (depends #660): race de persistencia ao fechar mid-stream
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
- 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
- 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
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
- HomeSearchResultsShell renderiza 'Nenhuma sugestão ainda — priorize
municípios na planilha de projeção' quando o suggest responde success
sem hits (Início e FAB overlay compartilham o shell; SSR initialSuggest
e POST cobertos); gateada por !isFetching para não falar sob o skeleton
- B126 e2e determinístico: pin um município priority alta claimado;
testes OPS29 novos usam advisor sem carteira (scoped, imune a alta
global de workers paralelos); settle de animação do overlay no B126
- unit: 3 casos novos no shell + overlap skeleton/mensagem
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
Estilo de links no markdown do chat (primária + sublinhado + hover/foco),
externos abrem em aba nova; compõe com o next/link interno do B188.
Sem migration; e2e + unit cobrem a superfície.
- src/lib/sollinhaOpeningQuestions.ts: catálogo puro de chips de abertura
(staff: 4 perguntas ancoradas nas tools atuais; leader: conjunto seguro
sem tool eleitoral — fail-closed B180)
- ChatChipGroup: primitivo presentacional reutilizável (slot compartilhado
com o B192)
- role do usuário flui do layout server pelo shell até o context do chat
- slot acima do input renderiza só com conversa vazia (messages 0 + ready);
tocar envia via sendMessage e os chips somem no primeiro envio
- unit tests do catálogo + e2e (staff desktop 4 / mobile 3 / leader seguro)
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).
- P1: B167 settle forçava open=true no load desktop mesmo com sessão restaurada
open:false (storage auto-curava para true); restore agora vence quando existe
- prune: cabeça assistant quebra convertToModelMessages; drop até ancorar em user
- MarkdownLink: node do react-markdown não vaza para <Link>/<a>
- isUIMessage: parts com shape inválido falham-closed no restore
- B167 mock: chunk legado {"type":"text"} falha schema v7 (status preso em erro);
agora usa start/text-start/text-delta/text-end/finish
- sessionStorage por aba: mensagens + estado aberto, restaurados no reload;
escrita só em estado settle (nunca mid-stream); guardrail 50 msgs / 256 KB
- links internos (/campanha…) do markdown navegam via next/link (sem reload)
- e2e novo + ajuste do B167 (visita mobile nova agora loga em viewport mobile)
- useAgendaKeyboardNavigation: ArrowLeft/Right → prev/next no mobile
(FullCalendar v7 não tem setas no grid — verificado no bundle); skips
para modificadores/dialog/menu/inputs; restore de foco na região
- Região de teclado mobile-only: tabIndex/role/aria-label gateados por
isMobile (desktop mantém a toolbar e sua árvore a11y intacta)
- onTitleClick ganha icon?: LucideIcon (glyph da página, aria-hidden);
agenda passa RotateCcw como affordance do 'voltar para hoje'
- E2E: teste de teclado (dia e mês), glyph no título, hardening do swipe
CDP (pacing 25ms — burst era classificado como flick pelo Chromium e
disparava back-nav ~35% das runs)
- Unit: 14 testes do hook/pura + 4 do renderer do glyph; impl plan
docs/plans/c101-ux-pos-critique-impl.md (aprovado pelo humano)
Closes#512
- toolbar do FullCalendar some no mobile; título do header mostra o período
("9 Agosto", "3–9 Agosto", "Agosto", "Agenda") e tap volta ao hoje
- navegação por arrasto horizontal (useAgendaSwipeNavigation): limiar 48px,
claim 12px com preventDefault, touchcancel sintético contra o long-press
do FC, dateClick do dia suprimido (C91/C15 intactos)
- filtro vira strip edge-to-edge sem rótulo, sticky sob a barra do app
(containing block via :has no padding do scroll container)
- visão de dia com scroll interno (height fixa), 08:00 na abertura,
cabeçalho do dia fixo, scroll preservado ao trocar de dia
- desktop inalterado; impl plan + plano pós-critique (C101-ux)
Motor client-side (standalone-iOS only, no-op em aba/desktop):
heal imediato no install + launch/bfcache/resume/pós-teclado/resize,
scrollTo(0,0) + re-measure nudge, retry ≤3×250ms por episódio,
pinça preservada (janela renovada por gesture*/touch ≥2), debug
global para verificação em aparelho, fallback maximum-scale off.
40 testes unit (jsdom) pinam detecção, guards, retry e teardown.
Troca o datetime-local nativo (AM/PM no idioma do browser, horário pouco
confiável) por ActivityDateTimeField: trigger com rótulo civil
dd/mm/aaaa às hh:mm + popover com Calendar (react-day-picker 9.7,
timeZone America/Bahia, locale pt-BR) e selects Hora 00-23 / Minuto
em passos de 15. Contrato de valor e submit inalterados; prefill do
slot mantido. Novo ui/Calendar.tsx (variante radix-nova), helpers
puros em campaignTime.ts. Sem migration. Closes#482 (PR).
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
- Colunas responsivas ao painel via @container/territory-list (P0→P3), sem scroll horizontal
- Contagem de municípios no nome ("Irecê (20)"); % e votos absorvidos na célula 2022 (hover = válidos)
- Célula 2026 no detalhe de Municípios (valor central + %, faixa de cenários no hover, leitura)
- Colunas de rede só leitura: Assessor, Liderança, Dobradinha (para todo o staff)
- Cobertura oculta por padrão no seletor; labels curtos (2022/2026/Captura/Cobertura)
- loadTerritoryOverview devolve { rows, references }; rollup puro com soma por cenário, hasEstimate e uniões de rede
Substitui o placeholder B164 por um feed operacional da carteira (staff):
- Cards com avatar do autor, polaridade/urgente/adversário, município e data
- Filtro omnibox (município via dotted-path, polaridade, urgente, autor, busca)
no padrão das listas (URL canônica + adapter puro)
- Criação in-place: modal desktop / bottom sheet mobile, reusando o registro
unificado (C87) com prefill do município do filtro ativo
- Leader lockdown intacto (gate staff + canReadMunicipalityUpdate fail-closed)
- parseMunicipalityUpdateFormData único para os 3 forms de atualização
- Loader cruzando a carteira sem cláusula de município (access por ator)
- Int/unit specs (19 unit + 5 int) + convenções
Fix real: o modal de criação não pode ser filho React do <form> de filtros —
React 19 não liga progressive enhancement em action-form aninhado.
Ações de ajuste (votos, tendência, atualização, liderança) agora retornam à
origem ao salvar (toast + from allowlist B110 / Início), sem avançar sozinhas
para outro wizard. Removidos fila de encadeamento (wizardActionChain.ts),
param entry, skip/Pular (chrome mobile sempre X) e prefill de nota sem
produtor. Liderança: botão pós-save 'Continuar' → 'Concluir' (origem).
Sem migration.
Mobile bottom nav (staff only): 5 items (Início, Municípios, Atualizações,
Agenda, Mais) with Drawer overflow for secondary destinations. Leader and
desktop remain sidebar-only. FAB + scroll padding adjusted for safe-area.
Resolves#400
- Add calendarFeed collection with secret slug credential
- Add iCal feed endpoint at /campanha/agenda/ical/[secret] (unauthenticated)
- Add server actions for creating and revoking calendar feeds
- Add CalendarFeedButton UI component in agenda filters bar
- Add unit tests for iCal generation
- Fail-closed access: feed re-evaluates creator's municipality scope on each read
- Leader lockdown preserved; no PII in feed output