teqo/docs/changelog/2026-08-19-s11.md
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

4.3 KiB

S11 — o feed do Instagram configurado sem aparecer na home fica diagnosticável: fim do fail-closed silencioso da S3. A assessoria vê, na mesma global onde preenche o token, o estado da sincronização (instagramSyncStatus jsonb hidden na social-feed-settings) e o porquê de os posts não aparecerem — o painel InstagramSyncStatusPanel (campo ui irmão do picker) mostra 5 estados fiéis ao rascunho aprovado (desativado / ainda não configurado / falha com motivo em linguagem de produto + "Tentar sincronizar de novo" / sincronizado · há X min · N posts / aguardando 1ª sync). A causa da ocorrência de produção (board vazio com token+ID preenchidos) é agora confirmável em 1 clique após o deploy. Engenharia: loadInstagramFeed passou a parsear o body de erro da Graph API e lançar InstagramApiError (status + {message,type}); describeInstagramError mapeia para copy pt-BR com a correção — OAuthException/expired → "token inválido/expirado ou emitido via Facebook Login — gere pelo Instagram Login" (o refresh endpoint da Graph API só renova tokens de Instagram Login), "Invalid user id" → "confira o ID da conta Business/Creator", 5xx → indisponível, TypeError → rede. Estado escrito por 3 caminhos: render da home (getInstagramFeed, dentro do unstable_cache, SQL direto sem hooks — padrão anti-revalidateTag-no-cache da S2), afterChange da global (sync awaited quando a credencial IG muda — instagramCredentialsChanged(doc, previousDoc), gate que pula save de exclusões/YouTube; o sync é importado lazy no hook para o payload.config.ts não puxar o módulo do feed no load, que é o que todo spec int importa) e a rota POST /api/social-feed/sync (botão; payload.auth sessão admin exigindo users + isSameOriginRequest CSRF — allowlistada no spec de convenção de POST routes com razão, é auth de admin ≠ envelope /campanha). Bug real descoberto e corrigido na entrega: o afterChange roda DENTRO da transação do save (a row lock fica presa até os hooks retornarem), então escrever o status/snapshot via pool numa segunda conexão deadlockava no lock da própria linha (~80-230s por save, transações-zumbi segurando o lock). A correção roteia os persists do sync-hook pela mesma transação (getPostgresTransactionDatabase — o helper transacional da C6) com um req opcional no syncInstagramFeed; o render e a rota sem transação aberta seguem no pool. Diagnóstico reproduzido em pg_stat_activity (UPDATE transactionid Lock vs idle in transaction) e fix verificado (45ms vs 232s). Stub IG ganha estado invalid-token (400 OAuthException) para o e2e da cena de falha; runbook docs/ops/instagram-feed-token-runbook.md (o que o painel mostra, validação via curl /me e refresh_access_token, geração pelo Instagram Login, o que NÃO fazer). Migration 20260820_003426_add_instagram_sync_status (ADD COLUMN jsonb nullable, não destrutiva). /simplify (3 reviewers): absorvi a correção P1 de diagnóstico — o mapeamento de ID exigia só "user" (regex frouxo) e mensagens reais de token OAuthException ("the user must be logged in", "user changed password") caíam na branch de ID, mostrando a razão errada; passei a exigir "user" E "id" juntos (isInvalidUserIdMessage) e adicionei o mapeamento de AbortError (timeout → "demorou demais"). Painel: gate passou a respeitar o master enabled (master off → "Instagram desativado", nunca "Sincronizado no board"), rótulo do botão no estado de primeira sync ("Tentar sincronizar" vs "de novo"), outcome transiente limpo ao editar credencial e aria-live nos boxes interativos. Testes: caminho transacional do syncInstagramFeed (persist na mesma transação) + fallback pool cobertos por unit; e2e do painel ganhou as cenas fail-500 e "desativado"; cleanup do stub global virou afterEach. Débitos registrados: #124 (janela de lock: fetch de rede no sync-hook segura a row lock até 10s, acoplando a home ao save) e #125 (polish P3 — tipo de persist duplicado, rota 200 vs 400, last-writer-wins stale, 5 posts hardcoded). Verificação: unit 2463 + int 791 verdes; lint/format/tsc/cycles verdes; pnpm build ok; e2e local — smoke admin 5/5 (painel ok → falha token → retry → falha 500 → desativado → não configurado) + describe serial frontend 19/19.