The 'advisor scopes municipalities' journey casts many steps; the dossiê
leadership-list chunk can land after goto settles under 4-worker load,
tripping the leadership-link visibility assert. Adds the C106 S: settle
before the assert and scopes with .first() against transient duplicates.
The B196 'relation groups' spec asserted the '1 município encontrado'
completion footer with the 10s expect default; under 4-worker CI load the
RSC list chunk can land late ('element(s) not found'), the same
stream-latency class as the issue's three deploy-blocking assertions.
Polls the footer with the same 30s budget the sibling geometry asserts
use, scoped to .first() so a transient streaming duplicate cannot trip
strict mode.
The FAB in campaignPermissionProfile:85/:101 (carteira/tudo write present)
and the advisor checkbox in campaignMunicipalities:115 (editar page) failed
consistently under 4-worker CI load because the RSC streaming shell's
S:* copies hadn't settled when networkidle resolved, and the default
expect/action timeout wasn't sufficient on the constrained hosted runner.
A probe proved the FAB SSR-renders correctly for carteira/tudo advisors
(profile scope is honored; no seed false-negative). The fix adds the
C106 shell-settle (document.querySelectorAll('div[id^=S:]').length === 0)
before the presence assertions, matching the established idiom used by
the sibling campaign spec tests.
21/21 passed at E2E_PROD=1 PLAYWRIGHT_WORKERS=4 repeat-each=3.
gate:fast (2439 unit tests) all green.
Fix manual dispatch em 18 formulários de /campanha que usavam <form action={submitAction}>: campos não controlados voltavam ao defaultValue após qualquer action assentada (sucesso ou erro) do React 19.
- WizardStepFormChrome alterado uma vez (cobre 3 wizard steps)
- 15 forms recebem dispatch manual arquivo a arquivo
- AdvisorPasswordResetButton (2 micro-forms sem campo digitável) ficou como está
- Unit spec novo campaignWizardStepFormChrome.unit.spec.tsx
- CHANGELOG-AGENTS.md cleaned (98 duplicates removed)
Also fixes:
- campaignPageActor writable gate only blocks advisors (was blocking coordinators too)
- campaignLeaderships e2e stability (waitForLoadState before attribute assertion)
A home é dona do container de scroll interno (h-dvh overflow-y-auto, body
overflow-hidden), então a navegação nativa por fragmento não aterrissa no
alvo; no mobile o reflow do h-dvh + imagens eager do hero deslocam o layout
após o scroll inicial.
Novo componente cliente CampaignHashScroll (renderiza null) montado no
layout da home: retenta o hash inicial até a seção entrar no viewport
(parando na primeira interação do usuário) e ouve hashchange para âncoras
in-page. scrollIntoView sem behavior herda scroll-smooth/motion-reduce.
E2E novos: describe mobile (load direto, CTA, reduced-motion) + load direto
desktop em describe próprio (evita corrida no beforeAll de consent).
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.
* deploy: limpeza pós-deploy do build cache e tags locais antigas (INF3/F2)
O deploy compila no homeserver e acumulava build cache + tags locais
antigas no disco raiz (incidente 19/08: 45G de cache). Após o smoke:
docker builder prune -f + remoção das tags locais teqo-1313 exceto o
SHA em uso (best-effort, registry preserva o rollback).
* 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.
* plans(C141,C143,C142): permissões granulares por assessor + responsáveis de demanda + UI por perfil
* plans(C141,C143,C142): permissões granulares por assessor + responsáveis de demanda + UI por perfil
* feat(S8): seção 'Nossa história' na home — vídeo da trajetória embutido do YouTube (Closes#91)
* docs(S8): resolução do conflito com S9 (história → newsletter) + nota do guard e2e dev-only
* feat(S10): pixel do Meta na página pública de campanha (campo admin em SiteSettings + PageView na home + Lead no sucesso da captura)
Campo tracking.facebookPixelId no global SiteSettings (mesma validação/normalização do precedente Petition, regra centralizada em facebookPixel.ts); home (grupo (home)) injeta o base code + PageView quando o ID está configurado (fail-closed) e o CampaignNewsletterForm dispara exatamente um Lead no sucesso do S9. Migration 20260819_213947_add_site_settings_facebook_pixel_id. Closes#94
* refactor(S10): simplify pass — Lead wiring unit-tested e raças de estado compartilhado do e2e resolvidas
e2e: stub hermético de connect.facebook.net no guard compartilhado (specs paralelos renderizam a home com o pixel); advisory lock no Postgres serializando o login do usuário de teste (Payload perde sessão em login concorrente read-modify-write — classe documentada no webauthn); seedTestUser vira create-if-missing (sem janela de usuário ausente nem reset de senha); Lead do form protegido do catch de erro (tracking nunca falha uma captura commitada); descrição do campo compartilhada; unidade nova campaignNewsletterLead cobre exatamente um Lead no sucesso.
* feat(C141): perfis de permissão por assessor — Visão × Edição configuráveis, enforcement fail-closed (Closes#105)
* chore(C141): regenera changelog agregado após rebase com S10
* plans(C144): options write-scoped para formulários de escrita pós-C141
---------
Co-authored-by: Francisco Solla <admin@solla.dev>
O fail-closed silencioso da S3 deixava a assessoria cega: token errado não deixava
rastro além do picker vazio. Fim da caixa-preta:
- `instagramSyncStatus` (jsonb hidden) na global `social-feed-settings`, escrito pelo
render, pelo afterChange (sync awaited quando a credencial IG muda) e pela rota
`POST /api/social-feed/sync` (botão, auth admin + same-origin).
- `InstagramApiError` + `describeInstagramError`: o fetch parseia o body de erro da
Graph API e o mapeia para copy pt-BR com a correção (OAuthException/expired →
"token inválido/expirado ou emitido via Facebook Login — gere pelo Instagram Login";
"Invalid user id" → confira o ID; 5xx → indisponível; TypeError → rede; AbortError →
demorou demais). O ID só casa com "user" E "id" juntos (token rejections real falam
"the user must be logged in" — não podem virar a branch de ID).
- `InstagramSyncStatusPanel` (campo ui irmão do picker): 5 estados fiéis ao rascunho;
gate respeita o master `enabled`; `outcome` transiente limpo ao editar credencial;
aria-live nos boxes interativos. Home inalterada (fail-closed intacto).
- Bug real: `afterChange` roda dentro da transação do save (row lock até os hooks
retornarem); persistir o status via pool numa 2ª conexão deadlockava no lock da
própria linha (~80-230s/save). Fix: persists do sync-hook na MESMA transação via
`getPostgresTransactionDatabase` (C6); render/rota sem transação seguem no pool.
Diagrama: UPDATE transactionid Lock vs idle in transaction; fix verificado 45ms vs 232s.
- Stub IG ganha estado `invalid-token`; runbook docs/ops/instagram-feed-token-runbook.md;
migration add_instagram_sync_status (jsonb nullable).
- Débitos registrados: #124 (janela de lock) e #125 (polish P3).
- Gates: unit 2463 + int 791; lint/format/tsc/cycles; build; e2e admin 5/5 + frontend serial 19/19.
- 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)
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.
O poll (request.get) convergia no HTML fresco, mas o page.goto servia a
cópia stale do cache heurístico do Chromium (ISR s-maxage sem max-age)
quando a regeneração pós-bust ainda não tinha completado — flake medido no
CI (YT snapshot da S2, 2 flaky) e no prod-mode local (IG snapshot 3/3
retries). cache:'no-cache' não existe nos tipos do Playwright 1.58 e reload
reduz mas não elimina a janela. URL única (?e2e=<ts>) erra todas as
entradas de cache (HTTP + router do Next) e a página sempre vem fresca do
servidor; prod-mode local 9/9 × 2.
Board "Acompanhe de perto" (S1+S2) ganha o feed automático do perfil oficial:
loader irmão src/utilities/socialFeed/instagramFeed.ts (Graph API /media com
children p/ carrossel + username, unstable_cache tag social-feed + revalidate
300, snapshot cru pré-exclusão persistido via SQL direto, refresh de token
automático com retry 1x — refresh endpoint só renova tokens de Instagram
Login, page tokens caem no fail-closed), global SocialFeedSettings estendida
(instagramEnabled/AccessToken/UserId/MaxItems + snapshot hidden; excludedItems
já cobria a plataforma desde a S2). Requisito nº 1 — picker de exclusão no
admin (primeiro componente custom do repo, campo ui + importmap): lista os
posts do snapshot com thumbnail e toggle "Não exibir" que escreve em
excludedItems. Card com coverAspect square (1:1 do rascunho), badge
Instagram, título = legenda com fallback para post sem legenda, clique no
permalink (noopener), link "Seguir no Instagram →" derivado do username.
Os feeds externos mudam para o subfolder socialFeed/ (pin da S2 previa).
Migration 20260818_192948_add_instagram_social_feed_fields (não destrutiva).
Stub IG no webServer (porta dev+2000, helper socialStub.ts) + 3 e2e novos no
describe serial + kill switch estendido + smoke do picker no admin.
Closes#20
Board "Acompanhe de perto" (S1) ganha o feed automático do canal oficial:
global SocialFeedSettings (admin-only, kill switch + toggle + maxItems +
exclusões por item), loader src/utilities/youtubeFeed.ts (Data API v3
search→statistics, unstable_cache tag social-feed + revalidate 300,
fail-closed com snapshot persistido via SQL direto — revalidateTag dentro
de unstable_cache lança no Next 15.4), card generalizado
(CampaignArticleCard → CampaignContentCard, external com noopener) e merge
por recência com o grande = mais recente elegível. Stub HTTP do YouTube no
webServer do Playwright (porta dev+1000) + 4 e2e novos no describe serial.
Migration 20260818_111748_add_social_feed_settings (não destrutiva).
Closes#18
Medido em probe: com export const revalidate, revalidateTag('posts') para
de regenerar a home (a rota /artigos, sem o export, convergia; a home
ficava stale para sempre). Remove o export e resolve o congelamento da
label relativa por threshold: relativa < 48h, data absoluta além (nunca
mente congelada). Spec: polling positivo do HTML da home pós-revalidate
(ISR serve stale uma vez sob carga) e about:blank antes do cleanup
(prefetch prod de links apagados = 404 no guard).
O warm-up único ainda deixava uma janela de stale-serve sob carga (a
regeneração ISR não completava entre o GET e o goto, e o DOM carregado
nunca refrescava — flake no cleanup sob a suíte paralela). Polling do
estado esperado no HTML do servidor (12x1s) antes do goto torna o
empty-state, o full-state e o cleanup determinísticos.
Sob a suíte completa em paralelo, a regeneração pós-revalidate às vezes
servia a página velha uma vez; o DOM carregado nunca refrescava e o assert
de cleanup falhava (1 flaky no gate). Requisição GET de warm-up entre o
revalidate e o goto torna o estado determinístico.
Bento 1+4 no desktop e carrossel 1/tela no mobile (~390px) lendo o post
collection com isPostVisible fail-closed; seção some inteira sem posts
visíveis. Zero migration/Consent; home vira ISR com tag posts +
revalidate=300 (label relativa não congela).
O data-time do FullCalendar é uma linha única que atravessa a semana; o
clique force no centro dela resolve o dia pela coordenada x -> coluna do
meio da semana (quinta). Com o término calculado como hoje+1 (C134), o
teste falhava seg/ter quando o término caía antes do início (alert de
validação no overlay). Passa a clicar na coluna do dia atual.
A primeira dobra da home pública agora acompanha as referências do Penpot
em 393, 768, 1024, 1280, 1366 e 1920 px. No desktop, a escala e a posição
horizontal das fotos dependem somente da largura: Solla, Lula e Wagner
ficam ancorados ao rodapé real do hero mesmo quando o conteúdo aumenta a
seção, e um palco centralizado de 1920 px preserva a proximidade entre
retrato e texto em monitores ultrawide. E2E fixa a geometria das seis
referências, transições de proporção, desktops baixos, ultrawide e os
overrides da WCAG 1.4.12; o caso de regressão também roda no Firefox.
O C139 moveu o leader tool de /campanha/contatos para /campanha/meus-contatos
(LEADER_CONTACTS_HOME); campaignConcepts e campaignHomeActions ainda
assertavam o redirect/link antigos — o gate e2e full pegou os dois.
O nome letters-only não carrega o runID, então o discoverOwnedRows do
ownership não encontra o contato criado via formulário (create row/sheet)
e a limpeza da fixture o deixava para trás — cada run vazava 2 contatos
no banco de teste. Deletar explicitamente por nome exato no fim do teste.
Cobre a página /campanha/contatos: recorte por omnibox + WhatsApp na
linha, create row desktop (erro de validação mantém o valor digitado,
conflito de nome com mensagem segura, create válido com POST), gate de
staff (líder cai em meus-contatos) e os sheets mobile (create via FAB
com telefone, edit com conflito + ficha atômica). Nomes criados via UI
são letras-only: o runID hex vira [a-p] (bijection, 128 bits).
O manifest ganha a família própria campaignContacts (antes caía no
campaignHomeActions).
O teste C104 clicava em '17 de agosto de 2026' fixo no picker de término;
quebrava deterministicamente ~2026-09, quando o dia sairia do grid visível.
Agora o alvo é civilDatePlusDays(formatBahiaCivilDate(new Date()), 1) —
sempre dentro do grid (dia do mês ou day 1 do mês seguinte como outside day).
Absorve também o flake pré-existente do redirect canônico ?tab=overview nos
3 sítios de toHaveURL do detalhe (mesmo regex da linha 219, C123).
O import do campaignFixtures.ts (que importa vitest) no fixture e2e crashava a
coleção no processo do Playwright: TypeError: Cannot redefine property:
Symbol($jest-matchers-object) em @vitest/expect — falha nos 2 shards e2e do CI.
purgeMunicipalityResidue/relationId/relationIds/RelationshipValue agora vivem em
tests/helpers/campaignResidue.ts (sem vitest), re-exportados por campaignFixtures
para os importadores existentes.
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.
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.
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.
O dropdown de adição de municípios (pessoas ×3, assessores, lideranças,
dobradinhas) oferece 'Salvador — Todas as zonas' como primeiro hit da busca:
um toque adiciona as 19 ZEs num lote, via máquina existente do RelationChipCell
(delta otimista, cap/floor, undo, colapso 'Salvador (19)' B178, exact-match C116).
- searchMunicipalityPortfolio ganha kind 'city' (NORMALIZED_ZONE_CITIES,
derivado do catálogo) com gate de escopo por contagem: o agregado só aparece
quando a cidade inteira está no escopo addableIds do ator — a label 'Todas as
zonas' nunca mente para assessor com carteira parcial.
- hitDescription ganha o branch 'Todas as zonas'; sem migration/access/schema.
- Testes: unit busca pura (19 ids, soma, escopo parcial, tudo atribuído,
posição), cell render, e2e em campaignPeople (agregado → chip → 19 zonas no
DB).
Review 3× read-only (0 P1):
- setup: /campanha/pessoas faltava (campaignPeople); log do 1º hit
quando o retry salva; comentário do budget ancorado no medido.
- login: sub-budgets 15+5+30 (antes 30+5+30 estourava o test timeout
e credencial errada morria de timeout genérico); retry bifurca pela
URL real (dashboard/fora do login/re-submit) — re-submit cego
contra página que saiu do login falhava com fill na dashboard.
- :713: retry cego criaria duplicata que o UNIQUE do slug refuta —
o retry consulta por título e navega ao detail quando o 1º POST
criou mas o redirect se perdeu.
- B24: locator reusa visibleMunicipalityButton (escapeRegExp +
fronteira de nome, 23/435 com prefixo) e ensureWideMunicipalityList
re-rodado pós-reload (padrão do irmão dobradinhas).
- :1063: waitForResponse com timeout 5s para o toPass decidir.
Re-prova: campaignMunicipalities 22/22 (arquivo atual, B193+OPS29 do
main) ×3 consecutivos em dev a 2 workers (load 5-17).
- setup.e2e.spec.ts: prewarm GET 13→26 rotas (rota /editar, alvos de
redirect, wizards, PWA artifacts, rotas do chat Sollinha) + POST
ganhou next-steps, home-search, ai-chat, ai-transcribe; prewarmGet
com retry-único (abort do compile aborta a conexão do próprio
prewarm); budget explícito 420s no lugar do test.slow().
- campaignE2EFixtures.ts: campaign.login com retry-único do submit
(re-fill+re-click com grace-wait do destino exato) — submit abortado
pelo full-page reload do compile frio nunca navega.
- campaignMunicipalities.e2e.spec.ts: refactor mínimo por evidência —
B24 (estágio largo B158 + toggle do trigger pós-reload), :713 (retry
do server action de criação), :1063 (seed parity via db:seed:minimal
— achado do OPS28 — + retry do suggest).
- P4-L fechado com evidência: campaignLeaderships ×3 dev + ×1
E2E_PROD=1 (TECH-DEBT row 59 + TESTING.md caveat).
- Aceite: campaignMunicipalities 17/17 ×3 consecutivos em dev a 2
workers (load 5-18).
Sem migration/access/Consent/UI.
- 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)
- 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)
- 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)
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
- 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
D12 (#641): strict-mode violation (2 triggers idênticos) só em runs
completos sob carga. Estado estável tem 1 match (verificado em browser);
o 2º match é duplicação transitória RSC-pending, mesma classe de cd469857.
Mesmo alvo em campaignActivity:52 (único outro uso do label em e2e).
- 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
- Sessão por arquivo: coordinator/leader compartilhados por worker (ownership
própria, cleanup no afterAll); beforeEach semeia o cookie campaign-token no
context fresh de cada teste; advisors permanecem por teste (OPS29/B176)
- Premissa da intenção corrigida por medição: getPayload é memoizado por
worker (364 ms cold, 0 ms warm); o overhead real era login de browser
(3.6 s quente / 30.3 s 1º hit vs 112-134 ms de seed) + criação de usuário
- Sem mode: 'serial' (divergência declarada): seed por cookie dispensa o
reuso de contexto e evita skips em cascata no relatório
- Endurece 7 interações client-side com toPass (classe B13/B17 exposta pela
remoção do warm-up do login) e 4 lookups do strip de ações com .first()
(duplicado transitório RSC)
- Medição antes/depois registrada no CHANGELOG; débito OPS39 (prewarm de
home-search/mudar-tendencia) registrado com depends no OPS30
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).
Chassis de lista (vale para todas as listas, gate A):
- scrollport: max-md:has-[.campaign-list-omnibox-form]:pt-0 — o containing
block do sticky passa a começar no topo; a barra cola no header (sem vão
de 16px, nada passa por cima no scroll)
- barra densa: campo min-h-8 py-0 + X size-8 → ~40px no total; anel de
foco escopado a md (mobile sem anel de cor; caret é o indicador)
- trailing cluster desktop-only por padrão (edgeToEdge removido)
- pt-4 md:pt-0 nas 6 listas com linha de ações antes do form (incl. organizacoes)
Card mobile (B193 densificado):
- nome 18px leading-tight com território a ~2px
- chips como blocos label-acima (Classe/Tendência/Nível, gate B) via ChipBlock
- avatares numa linha por grupo com overlap proporcional (flex-1 cells,
sem cap/wrap/…, overflow-hidden nas bordas) — prop wrap → overlapRow
- MissingAdvisorBadge com wrap para não clipar na célula do grupo
- densidade vertical gap-2.5/py-3; toques min-h-11 preservados
e2e B196 (6 testes) + regressão B184/B183/B193 verde.
- Sessão por arquivo: coordinator/leader compartilhados por worker (ownership
própria, cleanup no afterAll); beforeEach semeia o cookie campaign-token no
context fresh de cada teste; advisors permanecem por teste (OPS29/B176)
- Premissa da intenção corrigida por medição: getPayload é memoizado por
worker (364 ms cold, 0 ms warm); o overhead real era login de browser
(3.6 s quente / 30.3 s 1º hit vs 112-134 ms de seed) + criação de usuário
- Sem mode: 'serial' (divergência declarada): seed por cookie dispensa o
reuso de contexto e evita skips em cascata no relatório
- Endurece 7 interações client-side com toPass (classe B13/B17 exposta pela
remoção do warm-up do login)
- Medição antes/depois registrada no CHANGELOG; débito OPS39 (prewarm de
home-search/mudar-tendencia) registrado com depends no OPS30
- scene posicionado único (.activity-agenda-swipe-scene) envolve frame + dots/bars:
percentuais resolvem contra o box do frame, não o content (chevron/label inclusos)
- width: 100% no scene (fix do /simplify: align-items flex-end do prev colapsava a 1px)
- --swipe-frame-columns deduplica o gradiente de 14.285% (2 call sites)
- unit test estrutural do preview (month/day/week/list, chevron por direção)
- e2e novo: direction prev com frame visível (pina o fix do colapso)