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).
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).
- payload.config.ts: vercelBlobStorage -> s3Storage (@payloadcms/storage-s3)
com endpoint Garage (forcePathStyle, region garage); bucket privado — o
staticHandler do plugin streama os objetos em /api/media/file/<filename>,
mantendo o contrato de URL relativa das 40 rows pré-existentes
- src/utilities/mediaStorage.ts: resolveS3StorageEnv fail-closed (S3_* parcial
-> throw no boot com a lista exata; nenhuma -> storage local dev/test)
- seed-posts: remove pre-delete do @vercel/blob (S3 sobrescreve nativamente)
- worktree-env.mjs: s3EnvCopiedLines all-or-nothing (unit-testada)
- envs S3_BUCKET/S3_ENDPOINT/S3_REGION/S3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY
no .env.example/.env.test; cloud-setup.sh sem o sed morto do blob
- deps: -storage-vercel-blob -@vercel/blob +storage-s3; importMap regenerado
- docs: impl plan (revisão A->B: Garage v2 sem acesso anônimo na API S3),
AGENTS.md, changelog 2026-08-18-ops52
Job deploy em ci.yml (runs-on host na workstation) streama
scripts/deploy-homeserver.sh para o homeserver via SSH: build dos stages
runner+migrator com secrets do teqo-1313.env, push no registry local,
migrate pelo serviço de maintenance antes do rollout, up + smoke. Zero
secrets novos no Forgejo; HEAD guard + flock; rollback automático com
backup do compose; runbook docs/ops/teqo-1313-deploy.md; invariantes
unit do script (bash -n + estrutura).
Dump pg_dump -Fc do Neon no homeserver (/srv/hdd/backups/teqo-neon-pre-migracao/),
restore como a role teqo_1313 (o container já lia o banco preparado — sem mudança
de config). Neon intacto como rollback. Divergência da intenção (A/B validada como
B): destino = teqo_1313, não teqo_prod. Débito de media registrado (#10 → OPS52).
Workflows nativos em .forgejo/workflows/ (ci, ci-pr, agent-pool,
automerge, issue-transition, plan-issue, vercel-promote, archive),
removendo .github/workflows e .github/actions. Runner Forgejo v13 com
services Postgres por porta + DB_HOST via gateway do bridge. Libs de
agente sobre o novo cliente forgejo-api.mjs (zero dependências,
GITHUB_SERVER_URL/FORGEJO_API_URL), helpers de workflow plain-Node.
pnpm issue corrigido (await em função sync) e validado ao vivo. Testes:
forgejoApi.unit.spec.ts novo, pin do matrix do ci.yml atualizado.
Closes#1 (parte 1 — docs/issues/pool seguem na parte 2)
Débito colhido no simplify do C139: ~21 forms com action={submitAction}
apagam o digitado em erro (provado na sessão). Registrado como #740 com
depends em #728.
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 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.
- 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)