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
3.9 KiB
S2 — seção "Acompanhe de perto" da home de campanha (jorgesolla1313.com.br) passa a incluir os vídeos do canal oficial do YouTube (@JorgeSollaDep), estendendo o board da S1. Fonte automática servidor-side: global SocialFeedSettings nova (grupo Configurações, read/update admin-only — a chave da API nunca vaza pelo REST), com kill switch enabled ("Pausar feed"), toggle youtubeEnabled, chave + channel ID, youtubeMaxItems (1–5, default 3) e excludedItems (platform+id+motivo opcional — mesmo mecanismo/lugar da S3), migration 20260818_111748_add_social_feed_settings não destrutiva (editada para conter só o DDL — o migrate:create regenerava drift de migrations hand-written sem sidecar JSON de 20260811). Loader src/utilities/youtubeFeed.ts (irmão de posts.ts, pinado no spec de convenções): parsers/format puros (formatYouTubeViews pt-BR "12,4 mil"/"1,2 mi", thumbnails maxres??high??medium), fetch Data API v3 em dois passos (search → statistics, fetchImpl/YOUTUBE_API_BASE_URL injetáveis), unstable_cache tag social-feed + revalidate: 300 (TTL da entrada, não da página — o bug do S1 era page-level); fail-closed com snapshot persistido por SQL direto (payload.db.drizzle, sem hooks — revalidateTag dentro de unstable_cache LANÇA no Next 15.4, verificado no source; o snapshot cru pré-exclusão sobrevive a edições de settings por merge do update), exclusões aplicadas na leitura. Card generalizado CampaignArticleCard → CampaignContentCard (git mv; external?: boolean → <a target="_blank" rel="noopener noreferrer">); a seção mescla artigos visíveis + vídeos elegíveis por recência (grande = mais recente — aceite S2; degrade para o comportamento S1 sem config), meta "há X · N visualizações" (relativa < 48h, absoluta além), link "YouTube →" derivado do channelId; carrossel mobile renomeado para "Conteúdos recentes". next.config ganha i.ytimg.com + host local do stub no remotePatterns. Testes: 15 units novas (formatters, parsers, eligibility, fetch com fetchImpl fake); e2e com stub HTTP node (tests/e2e/youtube-stub.mjs, webServer do Playwright vira array, porta = dev+1000; POST /__stub/state alterna ok/fail; thumbnails servidas localmente) — 4 testes novos no describe serial da seção (full-state com exclusão + link externo + carrossel, fail-closed sem snapshot, snapshot persistente com maxItems mudando na API down, kill switch) + reset de settings no beforeAll; conventions specs atualizados (pin do youtubeFeed, settle após about:blank). Verificação: unit 2306 + int 791 verdes; e2e frontend 14/14 dev e 6/6 prod-mode (E2E_PROD=1 — valida TTL+tags+snapshot no build real); lint/format/tsc/knip/cycles/build verdes. /simplify 3 reviewers (sem P0): P1 corrigido — label do kill switch invertido ("Pausar feed" marcado = ativo → "Feed ativo"); P2 absorvidos (className do link do header extraída, Promise.all posts+feed no cache frio, comentário do sentinela 0 do merge, helper único tests/helpers/youtubeStub.ts para a porta do stub, waitForHomeSectionState fundido no waitForHomeHTML com orçamento 12s do S1, precondição do persist documentada); débito REGISTRADO (#40, prio:P2, depends #18): o validate do itemId lê data.platform que é sempre undefined (Payload passa o doc inteiro; verificado no source) — produção sem validação de ID de vídeo, e o e2e depende dessa mortandade (ids do stub de 20 chars); correção em dupla documentada na issue. Descartados com justificativa: ramo maxres do search (pinned por teste), tag social-feed fora da convenção global_* (precedente election-tse/municipality-catalog), remotePattern localhost/thumbs incondicional (decisão deliberada dos 3 modos e2e), persist não-atômico (comentado); achados visuais do design-vision mapeados para a Issue #33 (separação visual branco-sobre-branco, fora de escopo por decisão do gate).