# Changelog de contexto para agentes (fatias do AGENTS.md) Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agentes paralelos): "Known Gaps" e todos os blocos "Recently resolved". Histórico — leia só quando precisar do contexto de uma entrega passada; o sempre-presente fica no `AGENTS.md`. Itens novos de "Recently resolved" passam a ser registrados aqui (uma entrada curta), não no AGENTS.md. --- **Recently resolved (2026-08-10):** **B189** (Issue #526) — o Sollinha ganhou a tool **`getMunicipalitiesWithoutUpdate`** (`src/utilities/ai/tools/getMunicipalitiesWithoutUpdate.ts`): "quais municípios estão sem atualização há mais de X dias" (cobertura do acompanhamento) no escopo do usuário, com **contagem total exaustiva** (sem top N — cobertura é "tudo além do limiar"), **limiar declarado na resposta** (`limiarDias` + `criterio` interpolado; default 30, ajustável por pergunta via `days` 1–365, estritamente maior), lista ordenada do mais velho ao mais recente com "nunca atualizado" no topo (estagnação máxima, sempre incluídos), itens com `diasSemAtualizacao`/`cidade`/`regiao`/`tipo` (ZE vs município inteiro)/**assessores** (nomes em query própria `campaignUser` — `name` não é identity-gated; nunca `depth`). **Fonte de recência = `municipality.lastUpdateAt`** (campo derivado pelos hooks de `municipalityUpdate`, admin read-only — a semântica compartilhada da família B185 "última atualização de campo", nunca `updatedAt` do município; sanity local provou create→coluna→delete→null via hooks); filtro de recência em JS com `municipalitySignalAgeInDays` (helper E9 reusado — coerência rótulo↔limiar sem boundary de instante no `where`). **Gate staff inline** (leader → `{ error: 'Leitura de municípios negada.' }` antes de qualquer query, shape da B180); leituras `overrideAccess: false, user: ctx.user` (assessor = portfólio automático); **guard fail-closed de escopo vazio** (assessor fora do portfólio → shape vazio escopado sem leitura). **Resolver de escopo extraído** da B185 para `src/utilities/ai/tools/aiToolScope.ts` (`resolveAIToolScope` — Salvador primeiro, município por alias, território acento-tolerante; 2º call site da família; nomes de tipos alinhados à extração em voo da B186 para o merge dela vir trivial; contrato da B185 inalterado, spec existente cobre a regressão) e **loader de nomes compartilhado** `aiToolQueries.ts` (fundiu `loadNamesByIds` da B185 com o lookup local — don't twin). Seção nova no system prompt: declarar sempre limiar+critério, resumir Salvador ("7 das 19 zonas sem atualização há mais de 30 dias") e agrupar por assessor quando pedido (mesmo precedente "o modelo agrupa pela lista" da B185). Unit tests novos (`municipalitiesWithoutUpdateTool.unit.spec.ts`, 15): gate fail-closed com payload intocado, critério + borda estrita (31→incluído/30→excluído/`days:15`), ordem (nunca por nome, depois mais velho→recente), escopos acento/Salvador/município/erro, ids do escopo no where, escopo vazio fail-closed, nomes de assessores, tipo ZE. Sem migration/access/Consent/UI. Intenção [`sollinha-municipios-sem-atualizacao-recente.md`](plans/sollinha-municipios-sem-atualizacao-recente.md); impl [`sollinha-municipios-sem-atualizacao-recente-impl.md`](plans/sollinha-municipios-sem-atualizacao-recente-impl.md). **Recently resolved (2026-08-10):** **OPS21** (Issue #538) — `REVALIDATE_SECRET` adicionada na **Vercel production** (projeto `solla/jorgesolla`): diagnóstico D6 confirmado (env ausente em `vercel env ls production`; `POST /api/revalidate` respondia `500` "not configured" em prod, quebrando o runbook pós-seed/mudança direta de DB). Segredo gerado com `openssl rand -hex 32` (valor via stdin, nunca argv), `vercel env add REVALIDATE_SECRET production`; **fallback necessário:** o deploy existente congelava as envs em runtime → `vercel redeploy` do último deploy de produção (2×, pois o primeiro `env add` via pipe herdou o `\n` do `openssl` e gravou valor de 66 chars — removido e re-adicionado limpo). Validação real em prod: sem secret → `401`; secret errado → `401`; secret certo → `200 { revalidated: true, tag: 'posts' }`; tag desconhecida → `400`. Preview não adicionada (projeto sem Git conectado — `vercel env add ... preview` exige branch, precedente D6). Sem código/migration/access/UI. Impl [`revalidate-secret-vercel-production-impl.md`](plans/revalidate-secret-vercel-production-impl.md). **Recently resolved (2026-08-10):** **C108** (Issue #522) — sino de notificações: **abrir já lê** — `openCampaignNotifications` (action única) lista + marca tudo como lido em 1 round trip, substituindo `listCampaignNotifications` + `markAllCampaignNotificationsRead`; o badge zera na abertura sem clique e o botão "Marcar todas como lidas" deixa de existir. Painel limpo em qualquer tamanho: sem título "Notificações" visível (sr-only para a11y), sem contador, sem botão "Fechar"; desktop vira **modal central** (Dialog shadcn, X no canto superior direito, clique fora fecha — par responsivo Dialog/Drawer do C94 via `useIsMobile`); mobile segue bottom sheet com handle, safe-area e **skeleton de loading** (o painel não abre mais em branco durante o round trip). `markAllNotificationsRead` extraída para `src/utilities/notification/notificationList.ts` (owner-scoped com bypass documentado, idempotente, cap 200 mantido; int testado: marca só as próprias, deixa as do outro intactas); `readAt` saiu do contrato `NotificationListItem` (sem leitores após a remoção do destaque de não lida); copy de erro unificada em `CAMPAIGN_NOTIFICATION_LOAD_ERROR_MESSAGE`; `router.refresh()` re-sincroniza a instância irmã do outro header só quando algo foi marcado (race cross-tab documentado); foco visível nos itens e `role="alert"` no erro. P4-D atualizado no ledger — o batch sem transação agora dispara a cada abertura (self-healing). Testes: int (owner-scope + idempotência) e unit novos do bell (dialog limpo, refresh condicional, empty/error, sheet mobile). Sem migration, access, Consent ou schema. Intenção [`sino-notificacoes-leitura-auto-modal-desktop.md`](plans/sino-notificacoes-leitura-auto-modal-desktop.md); impl [`sino-notificacoes-leitura-auto-modal-desktop-impl.md`](plans/sino-notificacoes-leitura-auto-modal-desktop-impl.md). **Recently resolved (2026-08-10):** **B184** (Issue #514) — no mobile (<768px), o filtro de listas `/campanha` virou o **padrão sem moldura do chassis `CampaignListOmnibox` (B127) — todas as listas**: label oculta (`input` carrega o nome via `aria-label`), campo sem borda/sombra (`md:` restaura o desktop), **form do filtro sticky sob a top bar** com linha separadora edge-to-edge, "Limpar" vira **X circular dentro do input** (aparece com chips OU busca digitada; slot sempre renderizado com `invisible` para a largura não pular; hit target 44px + círculo visual 28px; live region própria "Busca e filtros limpos." — limpar só busca não navega e o pending region ficaria mudo para AT), botão texto "Limpar" desktop-only. **Por que sticky no `
`:** sticky não escapa do containing block (o pai) — a coluna do input dentro do wrapper curto do omnibox "grudava" ~12px e rolava junto (medido); o form é o único wrapper filho do `CampaignPageShell` (altura total). **Exceção agenda (C101, merge anterior):** a `/campanha/agenda` já tinha o strip sticky próprio (`activity-agenda-filter-strip`) — mantido; o chassis ainda a cobre no campo/X/Limpar. Em municípios, adicionalmente: **"Salvar filtro" vira icon button no header mobile** (slot C94/C95, `SaveMunicipalityFilterControl presentation='icon'`; popover de nomear intacto; nome de rename é o contrato 2.5.3) e **cards edge-to-edge sem borda** com uma linha separando (`border-b last:border-b-0`, `-mx-4`, gate `md:`). "Nova atualização" (atualizações) — único trailing visível no mobile — foi para o header (mesmo padrão); `campaignMobileHeaderIconClassName` extraída (3º consumidor). E2E novo em `campaignMunicipalities.e2e.spec.ts` (390×844): label oculta, borda 0, sticky, X (limpa chips+busca, foco volta sem reabrir popover), salvar→renomear no header, cards com separador, desktop invertido (1280). Débito registrado no caminho: **D7** (sticky dos headers da tabela desktop morto — wrapper `overflow-x-auto` é o scroll container; medido). Sem migration, access, Consent. Intenção [`municipios-mobile-sem-moldura.md`](plans/municipios-mobile-sem-moldura.md); impl [`municipios-mobile-sem-moldura-impl.md`](plans/municipios-mobile-sem-moldura-impl.md). **Recently resolved (2026-08-09):** **C103** (Issue #504) — o formulário de criação rápida da agenda ficou **usável de ponta a ponta no celular**: no narrow (mesmo breakpoint 640px do calendário), o sheet de criação abre **do topo** (`swipeDirection="up"` na primitiva `Drawer` — que já suportava top sheet; zero mudança em `Drawer.tsx`) preenchendo `100dvh-1rem`, o conteúdo **rola** num scroll container e o rodapé ("Mais detalhes" + "Salvar") é **fixo** — verificado em smoke com teclado aberto (viewport 280px: sheet encolhe via `dvh`, conteúdo rola 187/187px e o Salvar permanece visível). O form ganhou `variant: 'popover' | 'sheet'`: no sheet, **labels viram `sr-only`** (a11y e `getByLabelText` intactos), placeholders dão contexto ("Adicionar título _", "Município _", "Local (opcional)", "Adicionar responsáveis"), obrigatórios marcados com asterisco (título/município no placeholder, Início no trigger), campos empilhados **sem gaps com `divide-y`** e inputs/triggers sem borda (lista iOS do canvas). O **seletor de data/hora abre como bottom sheet aninhado** no mobile (`ActivityDateTimeField` ganhou `isNarrow`/`label`/`required`: conteúdo compartilhado Calendar+selects renderizado em `Drawer` com header "Início — " + "Pronto"; desktop continua popover). Props backward-compatible: `placeholder`/`className` no `StrictCombobox`, `labelClassName`/`emptyText`/`triggerClassName` no `ResponsibleMultiSelect`. Decisão do smoke: swipe handle **removido** do top sheet (primitiva o posiciona no rodapé, contradizendo o gesto de swipe-up; continua nos bottom sheets). E2E mobile novo (viewport 390×844): sheet do topo (`y ≤ 1`), Salvar visível sem scroll, picker em bottom sheet com Pronto, evento aparece após salvar; unit narrow cobre placeholders/asterisco/Pronto. Desktop visualmente inalterado (B181 segue na fila para o clip do popover ≤720px). Sem migration/access/Consent/schema. Intenção [`agenda-mobile-form-criacao-usavel.md`](plans/agenda-mobile-form-criacao-usavel.md); impl [`agenda-mobile-form-criacao-usavel-impl.md`](plans/agenda-mobile-form-criacao-usavel-impl.md). **Recently resolved (2026-08-09):** **D6** (Issue #521) — push no device fechado de ponta a ponta. **Diagnóstico com evidência por elo** (leitura read-only de prod): (1) **envs VAPID ausentes no Vercel production** (verificado com `vercel env ls production`) — elo raiz: sem elas o servidor nunca chama o web-push e o card de perfil desabilita "Ativar"; (2) consentimento `campanha-notificacoes-push` já publicado (Onda 0, 2026-07-19); (3) 0 inscrições `pushSubscription` (bloqueado pelo elo 1); (4) 74 `Notification` geradas no sino (eventos fluem) mas **zero envios**; (5) banner no OS = passo humano (Android). **Dois defeitos de código corrigidos com evidência:** (a) o SW `/campanha/sw.js` servia `CACHE_NAME "campanha-dev"` em prod — deploys são CLI `vercel build --prebuilt` (git builds ignorados), e a rota `force-static` congelava o buildId no build onde `VERCEL_GIT_COMMIT_SHA`/`VERCEL_DEPLOYMENT_ID` não existem; a rota virou **`force-dynamic`** (lê `VERCEL_DEPLOYMENT_ID`, garantido em runtime) → cache versionado por deploy (unit test pina `dynamic`); (b) **subscribe não idempotente** — segundo toque em "Ativar neste dispositivo" no mesmo device devolvia a mesma subscription do browser e estourava o UNIQUE do `endpoint` (erro falso com o device já ativo); `subscribeCampaignPush` agora faz find-before-create e responde sucesso. **Ferramenta nova:** `pnpm db:check:push-chain` (`scripts/check-push-chain.mjs`, read-only — `PROD_DATABASE_URL` obrigatória, conexões com `default_transaction_read_only`) fecha um veredito por elo (SW servido/versionado, envs, consentimento, inscrições por usuário, log de eventos) com exit code. **Ops:** par VAPID gerado e adicionado em **Production** (`VAPID_PUBLIC_KEY`/`VAPID_PRIVATE_KEY`/`VAPID_SUBJECT`/`NEXT_PUBLIC_VAPID_PUBLIC_KEY`); preview ficou de fora — o projeto não tem Git repo conectado (deploys CLI) e `vercel env add ... preview` exige branch (`branch_not_found`); `NEXT_PUBLIC_*` precisa do build do próximo deploy para inliner. **Pendência humana:** teste em device real (Android Chrome) após o deploy — ativar em `/campanha/perfil`, disparar evento, conferir banner + toque; `db:check:push-chain` antes/depois. Sem migration, access ou Consent (fail-closed intacto). Intenção [`push-notificacoes-dispositivo-producao.md`](plans/push-notificacoes-dispositivo-producao.md); impl [`push-notificacoes-dispositivo-producao-impl.md`](plans/push-notificacoes-dispositivo-producao-impl.md). **Recently resolved (2026-08-10):** **C106** (Issue #517) — feed de atualizações `/campanha/atualizacoes` sem moldura no mobile: **cards edge-to-edge** (`-mx-4` no ul) sem moldura/arredondamento com uma linha separadora (`border-b last:border-b-0` — padrão exato dos cards do B184) + **empty state com copy única** "Ajuste o filtro ou registre um novo fato de campo." (descreve a ação, vale nas duas resoluções; o texto antigo citava o botão "+ Nova atualização"). O filtro sticky/sem label/moldura e o icon "Nova atualização" no header **foram entregues pelo irmão B184/C107 em `main`** (`campaignListOmniboxFormClassName` + chassis borderless < md + `campaignMobileHeaderIconClassName`, id `campaign-updates-new`) — no rebase, a versão do irmão venceu o conflito em `CampaignUpdatesFilters.tsx` (edit the owner, não twin; a hipótese original de CSS de página no padrão C101 foi descartada na execução). **Bug pré-existente corrigido no caminho:** o `@layer base` editorial aplica `my-6 ml-6 list-disc [&>li]:mt-2` a todo `ul` — o feed era a única lista campaign em `