teqo-backup/docs/plans/ops58-sincronizar-posts-prod-impl.md

246 lines
14 KiB
Markdown

# Impl: OPS58 — Atualizar artigos de produção com os novos posts do jorgesolla.com.br
Status: aprovado (gate humano 2026-08-18)
Atualizado em: 2026-08-18
Issue: #43
Intenção: docs/plans/ops58-sincronizar-posts-prod.md
Appetite restante: herdado (~0,5 dia — execução no homeserver + runbook)
## Leitura da intenção
- **Outcome:** todos os artigos do jorgesolla.com.br ainda ausentes em produção
criados como notícia publicada; os existentes intocados (create-only, sem
duplicata); capas dos novos servindo 200 em `/api/media/file/...`; cache
`posts` bustado; procedimento documentado como runbook (padrão OPS52-media).
- **O que NÃO negociar:** create-only (nunca edita/sobrescreve conteúdo
existente); PII intocada; contrato público de URL intocado; guard
`ALLOW_REMOTE_DB=true` explícito (fail-closed mantido); capas dos novos no
bucket de prod (env `S3_*` do stack obrigatório — nunca disco local).
- **O que reavaliar:** a hipótese "ferramenta existe, só executar" precisa de
dois ajustes — (1) o seed não tem modo **plan-only** (pre-flight de prod
inexistente; o precedente OPS52-media provou o valor de `--dry-run` no
homeserver); (2) a taxonomy hardcoded está **defasada**: o WP tem 43 posts e
o `CLASSIFICATION` cobre 39 — os 4 slugs novos incluem 3 de
pré-campanha/campanha que, sem a entrada, caem em `politica` **sem o tag
`eleitoral`**, furando o controle de visibilidade eleitoral do repo.
## Evidência coletada (2026-08-18, probes read-only)
- **E1 — Contagem do WP:** `x-wp-total: 43` (REST `_embed`, paginado —
`fetchArticlesFromWordPress` traz todos). `CLASSIFICATION` cobre 39 slugs →
**4 slugs novos não mapeados** (caem no default `politica` hoje):
- `bruno-reis-foi-covarde-detonou-solla-ao-rebater-o-prefeito-sobre-saude-de-salvador` (2026-08-17 — resposta política a ataque)
- `chapada-diamantina-recebe-caravana-liderada-pelo-deputado-jorge-solla` (2026-07-31 — caravana, pré-campanha)
- `lula-reforca-o-time-de-jeronimo-wagner-rui-e-jorge-solla-na-convencao-estadual-do-pt` (2026-08-02 — convenção estadual do PT, campanha)
- `o-pt-tem-que-disputar-o-programa-de-governo-conclamou-jorge-solla-em-plenaria` (2026-08-11 — plenária, campanha)
- **E2 — Risco do lock `eleitoral`:** os 3 de campanha acima sem tag
`eleitoral` ficam visíveis no site público mesmo se a equipe puxar o
controle `hidden` na janela eleitoral (lock do repo:
`isPostVisible` fail-closed esconde só posts com tag/categoria `eleitoral`).
- **E3 — Seed sem pre-flight:** `seed-posts.mjs` não ecoa o alvo (host do DB)
e não tem modo que planeje sem escrever; `recover-media.mjs` (OPS52) tem
ambos — padrão a espelhar (echo do alvo antes de qualquer escrita; modos
mutuamente exclusivos; argumento desconhecido morre).
- **E4 — Homeserver:** clone do repo em `~/teqo-deploy` (OPS53 — deploy doc;
o deploy deixa o clone em **detached HEAD** no SHA do job). Env do stack em
`~/stack/teqo-1313.env` com `DATABASE_URL=…@postgres:5432` — o `postgres`
só resolve **dentro** da rede `stack_default`; do host, o caminho comprovado
é o **proxy socat `teqo-1313-build-proxy`** que o deploy script cria
idempotentemente (publicado em `127.0.0.1:5433` no host — é exatamente como
o build o usa, `deploy-homeserver.sh:91-108`). O `S3_ENDPOINT=host.docker.internal:3900`
do stack só resolve dentro do compose; o Garage roda **no host** (o container
alcança via `host-gateway`) → do host, `S3_ENDPOINT=http://127.0.0.1:3900`
(a workstation/tailnet usa `http://100.119.220.31:3900`). A forma exata de
execução do OPS52-media não está registrada de forma consistente entre os
docs (o runbook dela menciona `cd ~/stack`; o deploy doc diz workspace
`~/teqo-deploy`) — o pre-flight do runbook abaixo valida a forma escolhida,
e a primeira execução registra o que funcionou na Issue.
## Abordagem recomendada
```mermaid
flowchart LR
A[seed-posts.mjs<br/>+ CLASSIFICATION 4 slugs] -->|pnpm db:seed:posts --dry-run| B[pre-flight prod<br/>plano sem escrita]
B -->|ok| C[SEED_MEDIA_CONFIRM=1 ALLOW_REMOTE_DB=true<br/>pnpm db:seed:posts] --> D[posts novos + capas<br/>Garage S3 prod]
D -->|POST /api/revalidate?tag=posts| E[bust cache posts]
E -->|media:recover --verify + curl páginas| F[aceite público]
```
**Opções consideradas (forma de execução):**
- **A — Estender o dono:** `--dry-run` (plan-only) no `scripts/seed-posts.mjs`
- classificar os 4 slugs novos no `CLASSIFICATION` + runbook no homeserver.
**Recomendada.**
- B — Executar o seed como está ("às cegas") e confiar no relatório final —
o relatório só diz created/skipped **depois** de já ter escrito em prod;
sem echo de alvo, sem chance de revisar "o que será criado" antes.
- C — Script novo de pre-flight separado — twin do fetch/lookup do seed;
conhecimento vazaria em 2 módulos.
**Opções consideradas (taxonomy dos 4 slugs):**
- **A2 — Expandir o `CLASSIFICATION` com os 4 slugs** (3 com
`tags: ['eleitoral']`, o bruno-reis em `politica`). **Recomendada.**
- B2 — Seed intocado: os 4 caem em `politica` sem tag `eleitoral` + triagem
manual pós-sync no admin.
- C2 — Bloquear o sync até classificar (abort no unmapped).
**Recomendação: A + A2** — o dry-run é o pre-flight que o OPS52 provou no
homeserver (custo baixo: o seed já tem todo o fetch/lookup; dry-run troca
`payload.create` por contagem), e a classificação dos 4 slugs é **aditiva e
create-only** (não toca posts existentes — só decide a categoria/tag dos que
serão criados). Os 3 slugs de campanha **precisam** do tag `eleitoral` para o
lock de visibilidade do repo continuar valendo — registrá-los como `politica`
pura seria criar conteúdo de campanha fora do controle eleitoral em plena
janela. A manutenção contínua (próximos slugs) segue fora de escopo, como a
intenção manda.
**Rejeitadas:** B (sem pre-flight — escreve antes de informar); C (twin do
seed); B2 (fura o lock `eleitoral`; triagem manual pós-sync é pior que a
classificação no commit, com os 4 slugs já em mãos); C2 (transforma 4 slugs
em gate de conteúdo — a intenção quer o sync desbloqueado).
### Componentes / mudanças
- **`scripts/seed-posts.mjs`** (extensão do dono — comportamento default
inalterado):
- Parse de args no topo (padrão `recover-media.mjs`): só `--dry-run`
aceito; argumento desconhecido → die; echo do alvo (`DB host:
<host>` — sem senha) antes de qualquer trabalho.
- Modo dry-run: executa o fetch + lookups de tags/posts existentes +
resolução de covers (`resolveCoverSource`, **sem download** de imagem),
e no lugar de cada `payload.create` conta/lista "would create" /
"would skip" / unmapped / sem-cover. Zero escrita. Relatório final
espelhando o SUMMARY atual.
- `CLASSIFICATION`: +4 entradas (E1) — 3 com `tags: ['eleitoral']`.
- **`package.json`:** nada novo — `--dry-run` é argumento do script existente.
- **Migration:** nenhuma. **Access / Consent:** nenhum. **UI:** N/A
(Impeccable A — sem mudança de UI; conteúdo/operação).
- **Docs:** runbook de execução em prod neste impl plan (ordem do homeserver);
entrada `docs/changelog/2026-08-18-ops58.md` + `pnpm changelog:build`;
nota curta no AGENTS.md (§ seeding de conteúdo) apontando o runbook.
## Fases verificáveis
1. **Seed (dry-run + taxonomy)** — parse de args, echo do alvo, early-returns;
`CLASSIFICATION` +4. Gates locais: `pnpm gate:fast`, `pnpm format:check`,
`pnpm exec knip`, `pnpm check:cycles`. Validação de comportamento:
`pnpm db:seed:posts --dry-run` contra o DB local do worktree — relatório
com "to create"/"skip"/unmapped e **zero** posts/tags/media criados
(conferir via contagem antes/depois).
2. **Docs + changelog** — runbook completo neste impl plan, nota no AGENTS.md,
`docs/changelog/2026-08-18-ops58.md`, `pnpm changelog:build` +
`pnpm changelog:check`.
3. **Gates finais + merge** — `pnpm gate:fast`, `format:check`, `knip`,
`check:cycles`, `pnpm test` (unit+int), `pnpm build` local; PR → CI verde
→ merge em `main` (deploy automático OPS53).
4. **Execução em prod (pós-merge, runbook — humano):** seção abaixo.
## Runbook de execução em produção (pós-merge, humano)
1. **Deploy do merge em `main`** (CI OPS53) — o seed com `--dry-run` e a
taxonomy chegam ao homeserver com a imagem do SHA.
2. **Pre-flight no homeserver** (no clone do repo `~/teqo-deploy` — o deploy
OPS53 deixa o clone em **detached HEAD**, então trazer para `main` primeiro;
env do stack sourceado; `postgres` só resolve dentro do compose → usar o
**proxy socat do build** em `127.0.0.1:5433`; o Garage roda no host →
`S3_ENDPOINT=http://127.0.0.1:3900`):
```bash
ssh homeserver
cd ~/teqo-deploy
git fetch origin main && git checkout main && git pull --ff-only origin main
pnpm install # se preciso (hooks — padrão ensure-repo-deps)
set -a; source ~/stack/teqo-1313.env; set +a
export DATABASE_URL="${DATABASE_URL/@postgres:5432/@127.0.0.1:5433}" # proxy socat do build (postgres só resolve dentro do compose)
export S3_ENDPOINT="http://127.0.0.1:3900" # Garage roda no host (host.docker.internal só resolve no compose)
ALLOW_REMOTE_DB=true pnpm db:seed:posts --dry-run
```
Esperado: echo do alvo (`DB: 127.0.0.1:5433`), relatório com os 4 slugs
novos a criar (`would-create … (cover: url)` — URL presente; o download/upload
só acontece no sync), slugs existentes a pular, sem escrita. (O dry-run não
escreve — segue sem a flag `SEED_MEDIA_CONFIRM` do sync, mesmo com as `S3_*`
exportadas.) Se o dry-run
falhar com ECONNREFUSED/ENOTFOUND: conferir o proxy (`docker ps | grep
teqo-1313-build-proxy` — o deploy script o cria idempotentemente; se
ausente, re-dispatchar o deploy ou rodar o trecho que o cria em
`scripts/deploy-homeserver.sh`) e registrar a forma que funcionou na Issue
(primeira execução do runbook).
3. **Sincronizar:**
```bash
SEED_MEDIA_CONFIRM=1 ALLOW_REMOTE_DB=true pnpm db:seed:posts
```
Esperado: `Posts created : 4` (os novos), `Posts skipped : 39` (ou o número
real de já existentes), capas baixadas e uploadadas para o Garage de prod
(`S3_ENDPOINT` já reescrito no passo 2). **Nota (OPS60):** o sync escreve
media no bucket das envs `S3_*` — o guard `SEED_MEDIA_CONFIRM=1` é
obrigatório (fail-closed; sem ele o seed recusa com exit 1 antes de
qualquer escrita). Se o WP ganhar slugs novos entre o
pre-flight e aqui, eles entram como `politica` com warning — registrar na
Issue e classificar (admin ou follow-up).
4. **Bust de cache:**
```bash
curl -X POST https://jorgesolla1313.com.br/api/revalidate \
-H "x-revalidate-secret: $REVALIDATE_SECRET"
```
Esperado: `200 { revalidated: true, tag: 'posts' }` (o smoke do deploy
OPS53 já exercita o endpoint com o secret real — o env está no stack).
5. **Aceite:**
- Capas: `ALLOW_REMOTE_DB=true pnpm media:recover --verify` — esperado
`42/44` filenames 200: 38 das 40 rows pré-existentes (as 2 exceções
conhecidas do OPS52 — `fim-escala-6x1.jpg`, `jorgesolla.jpg` — seguem
404 e fazem o verify sair com exit 1; aceite: **as únicas falhas são as
2 conhecidas**) + as capas novas dos 4 posts (se o dry-run acusou
`cover: url` para os 4).
- Páginas: `curl -s https://jorgesolla1313.com.br/ | grep <slug-novo>`
e `curl -o /dev/null -w "%{http_code}" https://jorgesolla1313.com.br/noticia/politica/<slug-novo>`
→ 200 para os 4 novos; conferir também `/artigos` e a seção S1 da home
de campanha.
6. **Registro:** resumo na Issue #43 (created/skipped, exceções de capa,
slugs unmapped adicionais, forma exata de execução usada no passo 2).
## Rabbit holes / Não escopo (engenharia)
- **`--verify` de posts em código** — curl no runbook basta; não criar
ferramenta para o aceite.
- **Upsert de edits do WP / sync agendado / hidden flag** — fora de escopo da
intenção; itens separados quando doer.
- **Testar o sync ponta a ponta fora do homeserver** — impossível por
contrato (sem DB de prod, sem `S3_*`); a prova E2E é o runbook (padrão
OPS52 fase 3).
- **Unidade de teste para o parse de args do seed** — mesmo padrão do
`recover-media.mjs` (parse inline, sem teste dedicado); o write path não
muda (create-only) e o dry-run é validado contra o DB local.
## Riscos e mitigação
- **R1 — Host do homeserver não alcança `postgres`** (DB só resolve dentro de
`stack_default`; a forma comprovada do host é o proxy socat do build em
`127.0.0.1:5433`). Mitigação: o pre-flight (dry-run) valida em segundos;
se o proxy estiver ausente, o deploy script o recria (re-dispatch); a forma
real usada na primeira execução é registrada na Issue.
- **R2 — Capa nova não resolve** (WP mudou/removeu a imagem). Mitigação:
post criado sem cover → card degrada para a banda cinza existente (sem
placeholder); exceção registrada na Issue; `media:recover --verify` acusa
(as únicas falhas aceitáveis são as 2 exceções conhecidas do OPS52).
- **R3 — Conteúdo eleitoral sem tag `eleitoral`** (se o gate rejeitar A2).
Mitigação: triagem manual no admin imediatamente pós-sync (passo 3 do
runbook vira passo de classificação obrigatório).
- **R4 — `REVALIDATE_SECRET` ausente no env do stack.** Mitigação: o smoke
do deploy (OPS53) já testa `/api/revalidate` com o secret real — presença
garantida; conferir no pre-flight se o curl der 401/500.
- **R5 — WP ganha slugs novos entre o commit e a execução** (fetch é live).
Mitigação: relatório final lista unmapped; runbook manda registrar e
classificar (admin ou follow-up) — nunca bloqueia o sync.
## Aceite de engenharia
- [ ] Aceite de produto da intenção ainda coberto: posts ausentes criados,
existentes intocados (create-only), capas 200, cache bustado, runbook
documentado (padrão OPS52-media)
- [ ] Invariantes AGENTS/engineering-standards: `ALLOW_REMOTE_DB=true`
explícito mantido; identificadores em inglês; zero access/Consent/
transação/migration tocados; edit the owner (seed estendido, sem twin)
- [ ] Testes/validação de domínio: `--dry-run` contra DB local (relatório sem
escrita); gates da checklist do AGENTS; prova E2E é o runbook do
homeserver
- [ ] Docs: runbook de execução em prod + nota AGENTS.md + changelog