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
3.8 KiB
S3 — seção "Acompanhe de perto" da home de campanha (jorgesolla1313.com.br) passa a incluir os posts do Instagram oficial, completando o board do plano-site §4.2 (artigos S1 + YouTube S2 + Instagram). Fonte automática servidor-side: loader irmão src/utilities/socialFeed/instagramFeed.ts (os feeds externos ganharam o subfolder socialFeed/ — o pin da S2 predizia exatamente isso quando a segunda plataforma entrasse; git mv do youtubeFeed + imports atualizados): Graph API (GET /{ig-user-id}/media com children{media_url,thumbnail_url} p/ carrossel + GET /{ig-user-id}?fields=username), unstable_cache tag social-feed + revalidate: 300, snapshot cru ({ username, posts }, pré-exclusão) persistido via SQL direto (padrão anti-revalidateTag-no-cache da S2), exclusões aplicadas na leitura, refresh de token automático (refresh-on-failure + retry 1x, token renovado persistido por SQL direto; nota honesta no admin: o refresh endpoint só renova tokens de Instagram Login — page tokens business caem no fail-closed + rotação manual). Global SocialFeedSettings ganha instagramEnabled/AccessToken/UserId/MaxItems + instagramFeedSnapshot (hidden) — excludedItems já cobria platform: 'instagram' desde a S2, sem migration no mecanismo; migration 20260818_192948_add_instagram_social_feed_fields não destrutiva (DDL revisado, sem o drift do migrate:create da S2). Picker de exclusão no admin (requisito nº 1 da intenção — posts de grade): primeiro componente custom do repo (src/components/admin/InstagramPostExclusionPicker.tsx, campo ui na global, path string + importmap regenerado — o import direto de @payloadcms/ui na config quebra o generate:importmap no tsx por causa do .css do react-image-crop; com path string o scanner resolve sem carregar o módulo); lista os posts do snapshot (thumbnail + legenda + data + ID) com toggle "Não exibir"/"Reexibir" + motivo opcional que escreve em excludedItems via useField. UI: CampaignContentCardData.coverAspect ('video' default — S1/S2 intactos; 'square' 1:1 do rascunho aprovado para IG), badge "Instagram", título = legenda (fallback "Publicação no Instagram" para post sem legenda — entra no automático para exclusão manual), meta = data relativa, clique no permalink (nova aba, noopener), link do header "Seguir no Instagram →" (copy do rascunho aprovado; username derivado da API — com API fora e sem snapshot o link some junto com os cards, divergência documentada do "YouTube →" que sobrevive por ter o channelId configurado). next.config ganha https://*.cdninstagram.com/**. Testes: 16 units novas (parsers com caption null/carrossel/item sem permalink, thumbnail por media_type, eligibility, fetch + refresh-retry com fetchImpl); e2e: stub irmão tests/e2e/instagram-stub.mjs (porta dev+2000; a derivação da porta virou tests/helpers/socialStub.ts — rename do youtubeStub.ts mantendo UMA spelling), 3 testes novos no describe serial (full-state com exclusão + fallback de legenda + aspect 1:1 + link do perfil + carrossel, fail-closed sem snapshot, snapshot persistente com maxItems mudando na API down) + kill switch estendido com IG configurado + smoke do picker no admin (seed do snapshot via REST, toggle, Salvar, verificação do excludedItems). Verificação: unit 2351 + int 791 verdes; gate:fast/lint/format/tsc/knip (exit 0 — o erro de load do payload.config segue o ledger P3)/cycles/build verdes; e2e local: describe serial 9/9 + smoke admin 1/1 (rodando os projetos em sequência — em paralelo com --no-deps o seedTestUser dos dois beforeAll colide no mesmo usuário e quebra o run, artefato da invocação local, não do produto; o CI roda na cadeia de dependências). Detalhe do debug: o clique via CDP do MCP de browser não dispara eventos de input (ambiente do MCP, não do app — el.click() e o runner real funcionam).