Commit graph

810 commits

Author SHA1 Message Date
Francisco Solla
1dc4c55a8d fix(campaign): C140 — manual dispatch in 18 forms prevents React 19 form reset on error
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)
2026-08-23 17:42:59 -03:00
Francisco Solla
a97cd56fce fix(campaign): desbloqueia deploy — gate 'writable' libera coordinator/candidate e FAB ganha data-slot do e2e (C142)
Dois bugs do C142 que o e2e full do verify do deploy pegou (o PR rodou só o blast radius):
- requireCampaignPageActor gate 'writable' checava advisorEditingAccess(user)==='none', que retorna 'none' para QUALQUER papel não-advisor — barrava coordinator/candidate de páginas de escrita (municipios/[slug]/editar redirecionava o coordinator pro detalhe).
- CampaignQuickActionsFab não renderizava data-slot='campaign-quick-actions-fab', o locator do e2e campaignPermissionProfile.
Regressões pino: campaignPageActorGate.unit.spec.ts (5 casos) + assert de data-slot no spec do FAB.
Verificado: tsc/lint/format/cycles, unit 2435, int 819, e2e local dos specs afetados (os 3 casos que falhavam no deploy passam).
2026-08-23 16:41:40 -03:00
Francisco Solla
67c213585c ops(OPS78): launch do worktree opencode roteia pelo Vercel AI Gateway com variantes low/high/max (Closes #784) 2026-08-23 16:08:12 -03:00
Francisco Solla
5b7448832b ops(OPS73-FOLLOWUP): fecha #760 — resíduos da race do usuário compartilhado no e2e (lock único de login) + docs drift 2026-08-23 15:54:49 -03:00
Francisco Solla
038758eb81 docs(OPS47): alinha plano do agent-pool à política main-based pós-OPS71
- agent-pool-orchestrator.md: PR --base stage + issue-done-on-stage-merge.yml
  + promote stage→main viram --base main + issue-done-on-main-merge.yml
  + sem promote; migration-lock → countOpenSchemaPrs; model-selection →
  agent-pool-models.mjs; agent-github.mjs → agent-forgejo.mjs; workflow
  marcado removido OPS65 (caminho real .forgejo/workflows/agent-pool.yml)
- agent-pool/SKILL.md: L79 sem 'Antes do promote'; L105 agent-pool-github→forgejo
- comentário/label: agent-pool-state.mjs 'merge in stage'→main;
  agentPoolState.unit.spec.ts label 'on stage merge'→main
- changelog 2026-08-23-ops47.md (Closes #735)
2026-08-23 15:23:52 -03:00
Francisco Solla
dddbd920e0 fix(OPS46-S1): hook ESM tenta resolução bare do next/cache antes do remap (Closes #789) 2026-08-23 15:02:16 -03:00
Francisco Solla
f029c6f86e fix(S12): deep link #novidades aterrissa no formulário no mobile
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).
2026-08-23 14:27:15 -03:00
Francisco Solla
a74a2dca9e refactor(OPS46): simplify pós-revisão — release do claim ao fim do cleanup (janela purge segura), gate da hook exige o par de flags, Set→boolean, guard setval/nextval\s*\( e spec de steal invariante-primeiro 2026-08-23 14:24:33 -03:00
Francisco Solla
89bf9d0854 ops(OPS46): e2e local coleta via hook ESM (next/cache + server-only) e allocator de município com registry anti-wrap (Closes #733)
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.
2026-08-23 14:02:19 -03:00
Francisco Solla
1c865bd822 refactor(campaign): E1 simplify — pin loading/retry/stale-quirk no spec e tipo local
Review estrutural + qualidade (sem P1): spec ganha 3 pins do contrato mais
sutil (loading em voo, retry pós-falha, quirk gate→not-ready com resposta
stale — comportamento herdado dos 3 originais, preservado); UseAsyncSearchOptionsResult
vira tipo local (export especulativo, knip types:error); docstring documenta
o quirk e o trim; plano registra o ajuste de execução.
2026-08-23 13:16:47 -03:00
Francisco Solla
9a66a23cf6 refactor(campaign): E1 extrai useAsyncSearchOptions (3ª cópia da busca com debounce)
DemandResponsibleMultiSelect (C143), ResponsibleMultiSelect (C90) e
AsyncSearchCombobox trocam o efeito duplicado (requestId/setTimeout 250ms/
loading/failed) pelo hook compartilhado src/hooks/useAsyncSearchOptions.ts,
com isQueryReady opcional preservando a semântica de query vazia de cada
consumidor. Comportamento preservado; unit novo do hook (renderHook +
fake timers); sem schema/server/migration.
2026-08-23 13:16:47 -03:00
Francisco Solla
0d483a08db feat(campaign): C142 UI respects advisor permission profile somente_leitura
- Pure helpers: advisorEditingScope, rowEditingAllowed in campaignAdvisorProfile.ts
- gate: 'writable' in requireCampaignPageActor for route-level enforcement
- Shared cells accept readOnly prop: CampaignInlineEditableCell, MunicipalityRelationEditor, PhonesFieldEditor, DemandDescriptionEditor, etc.
- Municipality pages compute editingScope via resolveMunicipalityWriteScope and thread it to list/detail/edit surfaces
- Home actions, FAB mount rule, quick actions registry accept editingScope parameter
- Demands, activities, supporters, organizations create buttons gated
- E2E spec: campaignPermissionProfile.e2e.spec.ts covering 3 profiles
- Unit tests: campaignAdvisorProfile, accessShared, campaignComponents updated
- gate:fast passed (tsc, lint, format, knip, cycles, unit)
2026-08-21 23:01:35 -03:00
Francisco Solla
c90f70aab4 ops(OPS76): tracker de Issues migra do Forgejo para o GitHub — GitHub como host único
Decisão (2026-08-21): o GitHub passa a ser a fonte única — main, branches,
PRs, CI via Actions E tracker de Issues. O Forgejo deixa de ser canal ativo
(git.e issue tracker congelados pós-convergência).

Data: 17 issues migradas do Forgejo para o GitHub (frontmatter id/depends/
serializes/priority/model preservados, labels mapeados, comment de origem
em cada uma; tabela Forgejo→GitHub registrada).

Tooling: github-api.mjs estendido com a camada de issues/labels/comments/
close/workflow-dispatch/contents (mesmo shape normalizado do forgejo-api);
18 scripts trocam o transporte forgejo-api → github-api; forgejo-api.mjs e
seu teste (961 linhas) removidos; teste githubApi.unit.spec.ts novo (7 casos);
issue-transition-on-merge.mjs renomeado e reapontado.

Flips pós-merge: issue-done e plan-issue-ready agora usam o GITHUB_TOKEN com
issues:write no repo (sem FORGEJO_API_TOKEN) e fecham/flipam Issues no GitHub.

Docs: AGENTS.md, AGENT-OPS.md e skills do fluxo vivo (work-issue, plan-issue,
project-status, capture-review-debts, worktree-next-issue, agent-work-issue)
apontam o tracker para github.com/fsolla/teqo/issues.

Gates: tsc/lint/format/knip/cycles/unit 2416 verdes.
2026-08-21 08:19:06 -03:00
Francisco Solla
c723d6accf
merge(OPS75): convergência Forgejo→GitHub — GitHub como fonte única
* 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>
2026-08-21 01:41:18 -03:00
Francisco Solla
3f74b696e1 feat(C141): perfis de permissão por assessor — Visão × Edição configuráveis, enforcement fail-closed (Closes #105)
(cherry picked from commit 60857ad34d)
2026-08-20 14:25:01 -03:00
Francisco Solla
3ce5a69555 feat(S11): feed do Instagram configurado não aparece na home — painel de status da sincronização no admin
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.
2026-08-20 00:54:58 -03:00
Francisco Solla
2cc8f2c14d feat(C143): demandas visíveis apenas para responsáveis explícitos + candidato/coordenador
- 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)
2026-08-19 23:55:07 -03:00
Francisco Solla
eebe81cd1e feat(S8): seção 'Nossa história' na home — vídeo da trajetória embutido do YouTube (Closes #91) 2026-08-20 02:19:58 +00:00
Francisco Solla
398a91e54a 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.
2026-08-20 02:19:58 +00:00
Francisco Solla
e504491dd8 fix(OPS71-FLIP): flip pós-merge silencioso no GitHub — PAT no arm do auto-merge, dispatch de recuperação, close da Issue, retry 410
Root cause (Issue #119): auto-merge armado com GITHUB_TOKEN mergea como
github-actions[bot] e o anti-recursion do GitHub nunca cria runs para o
evento closed — os flips pós-merge (issue-done / plan-issue-ready) não
rodavam em silêncio (PR #746). Arm agora usa AUTOMERGE_PAT (fail-closed);
workflow_dispatch de recuperação nos dois flips; forgejo-issue-transition
fecha a Issue (PATCH state=closed); forgejo-api retenta 410 (proxy respondeu
antes da origem) e loga corpo nginx de forma compacta.
2026-08-19 22:45:29 -03:00
Francisco Solla
02c1e4f36b feat(OPS71): Fase 2 — remover arquivos do fluxo Forgejo (workflows/scripts/specs) pós-validação do GitHub Actions
14 arquivos removidos: .forgejo/workflows/* (6), forgejo-pr-automerge.mjs,
forgejo-dispatch.mjs, ci-classify-production.mjs, lib/dockerignore.mjs,
lib/branch-protection.mjs + 3 specs órfãs. Consumidores ajustados:
test-affected-core (HIGH_RISK_EXACT), ciSkipInvariants (re-ancorado no
deploy.yml dispatch-only — deploy nunca automático), comentários com
referência morta (gate-ci, github-pr-automerge, agent-pr-ready-automerge).
Docs: AGENTS.md, runbook (rollback via git history), README (era GitHub).
Tracker intocado (forgejo-api/agent-*/issue-transition ficam).
2026-08-20 00:20:10 +00:00
Francisco Solla
f5862c9ba3 fix(OPS71): ciClassifyProduction hermético — env GIT_* limpa + assert do scratch repo (não polui o worktree externo) 2026-08-19 23:07:34 +00:00
Francisco Solla
0901d60d72 fix(OPS71): literal do required check = nome do check-run (checks) + docs (pin PR #742) 2026-08-19 23:07:34 +00:00
Francisco Solla
8820a4427f fix(OPS71): pins do forgejoApi determinísticos sob GITHUB_SERVER_URL do GitHub Actions 2026-08-19 19:17:19 -03:00
Francisco Solla
24a867dfc3 feat(OPS71): CI/PR/deploy no GitHub Actions — tracker de Issues permanece no Forgejo (Closes #97) 2026-08-19 19:17:19 -03:00
Francisco Solla
cd3f3e2a06 test(S7): pins e2e endurecidos — contrato de borda completo e capa de borda a borda medida 2026-08-19 19:39:09 +00:00
Francisco Solla
4a3c3b709c feat(S7): cards 'Acompanhe de perto' sem raio, borda, sombra ou chip; capa de borda a borda (Closes #90) 2026-08-19 19:39:09 +00:00
fbfbfb1329 Merge pull request 'S4 — Compartilhar conteúdos da home no WhatsApp' (#86) from S4-compartilhar-conteudos-da-home-no-whatsapp into main 2026-08-19 16:26:43 +00:00
Francisco Solla
39fb3b47f0 fix(OPS62-followup): âncora de relógio no fixture do municipalitiesWithoutUpdateTool — elimina o flake de ordenação por raiz
Causa raiz (reproduzida, refuta a hipótese da Issue): daysAgo() chamava
Date.now() por chamada; as duas daysAgo(40) de Beta/Alpha podiam cair em ms
diferentes sob carga, deixando Alpha estritamente mais novo — sortStale
ordenava por timestamp e o tie-break por nome nunca disparava (0.08% das
construções sem pausa; 100% com pausa de 1 ms; fixture ancorado: 0 em 2000).
Fix test-only: âncora fixtureNowMs no load do módulo (padrão prioritiesTool)
+ hoist do timestamp empatado (igualdade estrutural) + comentário.
Nada muda em produção — o sort real é determinístico sobre timestamps fixos.
Verificação: spec 30/30, unit 2405, int 791, tsc/lint/format/cycles verdes.
2026-08-19 13:07:30 -03:00
Francisco Solla
8bc8900e9d feat(S4): compartilhar conteúdos da home no WhatsApp 2026-08-19 12:50:19 -03:00
Francisco Solla
ccfa60c1f2 fix(OPS64): pin do waitForChecks contra rollup mentiroso + docs — OPS62 eliminou o secundário estruturalmente
O sintoma primário da Issue (deadlock do waitForChecks no próprio pending)
já tinha sido entregue pelo OPS61. O secundário — o rollup `checks` reportava
SUCCESS com um shard de e2e vermelho porque o Forgejo 9.0.3 reporta o
resultado do ÚLTIMO shard como o parent de matrix no `needs` (runs 604/607/610;
o `needs.<job>.results` do GitHub não existe neste runner) — foi eliminado
estruturalmente pelo OPS62 (mergeado durante a execução): ci-pr.yml e ci.yml
viraram jobs únicos sequenciais, sem matrix/shard, e o status do job `checks`
já é o veredito honesto da cascata.

O que este item entrega: (a) teste-pino no waitForChecks (cenário run 607:
rollup verde + shard vermelho → o CLI LANÇA "Checks falharam" — nunca confia
no rollup sozinho; invariante de defesa válido e que pegaria qualquer matrix
que volte); (b) docs honestas (agent-pr-workflow.mdc, AGENT-OPS.md) com a
narrativa do parent de matrix e da eliminação estrutural pelo OPS62. O script
ci-rollup-check.mjs e os edits de workflow planejados foram descartados no
gate (redundantes pós-OPS62). Unit 2392 verdes; gates locais ok.
2026-08-19 12:11:41 -03:00
Francisco Solla
cc6149191b ops(OPS67): retry com backoff nos fetchs do forgejo-api
Um fetch failed transitorio (rede/DNS/reset) matava o run do safety net
(agent-pr-ready-automerge) no meio da espera de checks. Retry no request()
do cliente: fetch reject retenta em qualquer metodo; body-drop em GET
retenta; 5xx (502/503/504) retenta so em GET (body cancelado antes do
retry); 4xx e body-drop em escrita falham fail-closed. 3 retries, base
300ms x2, jitter +-20%, console.warn pt-BR por retry; opcoes flat no
createApi (retries/backoffMs/jitter/sleepImpl). 12 specs unit novas.
2026-08-19 13:28:37 +00:00
Francisco Solla
8d33e89102 style: prettier — runbook/impl plan/unit test (OPS66) 2026-08-19 10:07:14 -03:00
Francisco Solla
228f2217c7 ops(OPS66): deploy — migrate antes do build do runner (deadlock de migrations em caminho estático) 2026-08-19 10:06:00 -03:00
Francisco Solla
9504c7ccf6 ops(OPS65): CI do main em janela de 30 min (gate .dockerignore fail-open) + matar agent-pool
- ci.yml: on push removido → schedule */30 + workflow_dispatch (fundido com a
  estrutura de job único sequencial do OPS62 X1)
- job gate (host, main-only, sem pnpm): revision do container rodando no
  homeserver (label org.opencontainers.image.revision — o compose pode mentir
  após rollback falho) + diff + classificador .dockerignore
- fail-open: revision desconhecida → suite roda (verde); incapaz de classificar
  → run vermelho (nunca blackout silencioso); deploy duplo-gated
  (needs: [checks, gate] + if explícito)
- deploy-homeserver.sh: guard 'already deployed' idempotente (mesma revision)
- agent-pool.yml removido; pool dormente (144 pnpm install/dia no tick */10)
- specs: dockerignoreMatch, ciClassifyProduction (incl. git-diff com repo
  scratch e exit codes), pins ciSkipInvariants/deployScript; HIGH_RISK_EXACT
- docs: runbook (janela/gate/verificação ao vivo/falhas), AGENTS.md,
  AGENT-OPS.md (tabela X1 fundida), skills pool dormente, changelog OPS65
- impl plan aprovado; intenção marcada entregue
2026-08-18 21:08:12 -03:00
Francisco Solla
46038d5a6a fix(e2e S3): goto da home com URL cache-busting no describe da seção de conteúdos
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.
2026-08-18 20:30:10 -03:00
Francisco Solla
a638a8acae feat(S3): seção de conteúdos da home de campanha inclui os posts do Instagram
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
2026-08-18 20:29:50 -03:00
Francisco Solla
ee31aeee4f fix(OPS62): services por nome na rede do job — Forgejo 9 não expande expressões em ports
Achado ao vivo (run 730): a expressão ${{ 5433 + (github.run_id % 100) }} no
ports do service não é avaliada pelo Forgejo 9 — o literal ia ao runner e o
job falhava em 1s. E portas fixas colidiriam entre runs concorrentes (o job
único segura os services pelo run inteiro). Correção: services sem publish
host, alcançados por nome na rede do job (postgres-int/postgres-build:5432)
— solução canônica do Actions, mata a colisão de raiz. Guards fail-closed
(LOCAL_HOSTS + assertTestDatabase) admitem os nomes dos services (hosts
internos do runner; hosts remotos continuam rejeitados); pins novos em
localHosts.unit.spec.ts e ciSkipInvariants (sem ports:, URLs por nome).
2026-08-18 23:26:13 +00:00
Francisco Solla
12a4fe1005 chore(OPS62): CI fail-fast estrutural — job único checks sequencial (X1)
ci-pr.yml e ci.yml deixam de ter jobs irmãos: 1 job 'checks' com steps em
série (guards → lint → format → typecheck → knip → cycles → unit → int →
build → e2e). Step vermelho interrompe o job, o run falha e o slot é
liberado — sem API de cancel (Forgejo 9.0.3 não tem), sem watchdog, sem
upgrade do Forgejo. Skips do ci-scope.mjs viram step id 'scope'; e2e em 1
processo com PLAYWRIGHT_WORKERS=4 (substitui 2 shards × 2 workers); 2
services Postgres por run com portas host derivadas do run_id (runs
concorrentes não colidem); contexto 'CI (PR) / checks' preservado (job id
checks — contrato do automerger OPS61); deploy OPS53 gated em needs checks.
Plumbing morto removido (e2eShardConfig/e2e_shards). Docs: AGENT-OPS,
TESTING.md, changelog. Impl: docs/plans/ops62-ci-fail-fast-forgejo-impl.md
(a intenção watchdog+upgrade fica como histórico do gate).
2026-08-18 23:26:13 +00:00
Francisco Solla
8c915e59fc fix(S6-FOLLOWUP): test:e2e:affected rodava a suíte completa — --no-deps desliga o closure de deps dos projects em runs filtrados
Causa raiz (medida com o binário 1.58.2, config real): commander parseia
posicionais e -g/--list funcionam; o que arrastava a suíte era o closure de
dependências do dev mode (setup<-campaign<-frontend<-admin): filtrar um spec
do frontend puxava campaign(160)+setup(2). run-e2e-affected.mjs agora gera
test:e2e <passthrough verbatim> --no-deps -- <paths> via helpers puros em
scripts/lib/playwright-e2e-args.mjs (unit-pinned); passthrough corrigido
(pnpm consome o -- e o path era descartado); run-e2e-affected.mjs entrou em
HIGH_RISK_EXACT. Docs: receitas de filtro no playwright.config.ts, AGENTS.md,
changelog.
2026-08-18 19:09:12 -03:00
Francisco Solla
8b98930670 fix(OPS61): gate do waitForChecks sem o pending do próprio job — achado ao vivo PR #67
O Forgejo posta o status do próprio job do safety net como pending durante o
run inteiro: o gate antigo (pending.length === 0) era insatisfazível e o CLI
nunca auto-mergeou nada — todo merge Forgejo até aqui foi manual (merged_by
do dono), inclusive a PR #52 com CI vermelho (narrativa da 'raça' corrigida).
O gate é: rollup CI (PR) / checks presente+verde (cascata inteira assentou),
ignorando o contexto do próprio workflow; mergeable false só após assentar.
2026-08-18 17:45:54 -03:00
Francisco Solla
ac11afecf3 ops(OPS61): flip de issues pós-merge com PAT + gate do rollup checks + branch protection real no Forgejo
- issue-done/plan-issue-ready: env FORGEJO_API_TOKEN (GITHUB_TOKEN 403 em
  Issues) + fork guard (checkout de fork roda script do fork com o PAT — RCE)
- forgejo-issue-transition: falha de flip vira exit 1 (job vermelho, nunca
  skip silencioso)
- waitForChecks: exige o rollup CI (PR) / checks presente+verde (raça PR #52:
  snapshot com jobs não-agendados parecia verde); mergeable=false só conta
  como conflito após a cascata assentar; timeouts 30→45min (CLI) e 40→55min
  (workflow)
- configure-branch-protection: contexto real CI (PR) / checks* (Forgejo casa
  por glob contra 'CI (PR) / checks (pull_request)'; literal 'checks' nunca
  casaria), idempotente GET→POST/PATCH, verificação pós-aplicação; decisão
  pura em scripts/lib/branch-protection.mjs
- docs honestas (AGENT-OPS, agent-pr-workflow.mdc, CUTOVER-MAIN-ONLY, README)
- testes: raça do rollup/rollup failure/mergeable pré-settle +
  branchProtection create/update/noop + glob real
2026-08-18 17:45:54 -03:00
Francisco Solla
cacb6165ad feat(S6): home de campanha sem carrossel no desktop — grade estática em ≥1024px, mobile preservado 2026-08-18 19:32:50 +00:00
Francisco Solla
fe5b11cf24 feat(S2): seção de conteúdos da home de campanha inclui os vídeos do YouTube
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
2026-08-18 15:23:50 -03:00
Francisco Solla
7bd9c9504f ops(OPS59): pre-push sem e2e — e2e roda uma vez, no CI (gate local fica com os outros testes) 2026-08-18 17:56:41 +00:00
Francisco Solla
a60b3880ca feat(S5): banda visual própria para a seção 'Acompanhe de perto' na home 2026-08-18 13:35:53 -03:00
Francisco Solla
228d5da9c8 ops(OPS52-media-guard): reconcile do media:recover exige MEDIA_RECOVER_CONFIRM=1 (guard de intenção explícita) 2026-08-18 12:45:32 -03:00
Francisco Solla
7b8e37e162 fix(ops OPS57): simplify — veto de draft vale em conversão mid-run; CLI valida base main; docs sem mecanismo fantasma 2026-08-18 15:24:21 +00:00
Francisco Solla
b78a390371 fix(ops OPS56): forgejo-pr-automerge — veredito de merge por re-read do PR (falso positivo 'já mergeado' com PR aberto) 2026-08-18 14:28:54 +00:00
Francisco Solla
45e29a7785 feat(OPS52-media): recupera os objetos de media de produção do WordPress para o bucket (pnpm media:recover)
- scripts/recover-media.mjs: CLI de reconciliação (reconcile/--dry-run/--verify)
  — lê rows de media + posts.coverImage, resolve a capa com a mesma lógica do
  seed (featured media, senão 1ª imagem inline), PutObject com key = filename;
  DB nunca escrito; fail-closed (S3_* completas, ALLOW_REMOTE_DB, echo do alvo,
  timeouts curtos, covers só do origin WP e image/*)
- scripts/lib/wpArticles.mjs: fetch ao vivo do WordPress extraído do seed
  (sem twin), helpers puros de resolução + row→cover mapping (unit-testados)
- seed-posts.mjs: importa o fetch da lib (comportamento idêntico) e usa
  resolveCoverSource em processContent
- runbook de execução no homeserver (impl plan), nota no AGENTS.md, changelog
Closes #10
2026-08-18 08:20:45 -03:00