From dbe77f08cc77a9a0dffa34093302144e4066fa46 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Fri, 25 Sep 2026 14:35:22 -0300 Subject: [PATCH] =?UTF-8?q?feat(C226):=20frame=20est=C3=A1tico=20nos=20v?= =?UTF-8?q?=C3=ADdeos=20da=20Central=20antes=20do=20play?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Peça de vídeo publicada e com arquivo passa a mostrar um frame estático leve no card público antes do play, sem nenhum request do arquivo antes do gesto (`preload="none"` do S27 intacto). O frame é um derivado privado em `contentMedia` (nome determinístico), gerado por um dono só — `ensureContentPieceFrame` — chamado pelo job C211 (que já tem o arquivo em disco: a extração sai de graça) e pela rota `GET /conteudos//frame` em miss, que cura o acervo anterior sem backfill, fila, collection nova ou migration. A porta reusa o gate de slug publicado e o proxy de mídia privada do `/midia`: o kill switch fecha as duas de uma vez. Sem frame possível, o slot mostra o estado neutro honesto com play e o marcador — nunca imagem quebrada, nunca retry. A projeção declara o frame por elegibilidade (vídeo com arquivo), não pela existência do derivado, então a cura é invisível para o cache da listagem e não há tag para bustar. O frame também aparece na página da peça e no card da home (variante compacta de 124px, cena 05). A passagem de design (tier primário) achou dois bugs que só o browser mostra: o glyph de imagem quebrada numa requisição que falha, e — ao esconder a imagem até decodificar — o frame sumindo para sempre no caminho mais rápido (uma imagem que carrega antes da hidratação não dispara `load` nem `error` de novo; o `ref` decide os dois desfechos). Também: o guard de falha de e2e aceita `RegExp` e confere os paths declarados antes do caso do favicon (todo 404 do browser traz a mesma mensagem), o manifest de e2e mapeia os módulos novos, e o job C211 passa o arquivo que já tem em disco em vez de rebaixar o vídeo. Design tier: central-conteudos-preview-frame-video Closes #1352 --- docs/changelog/2026-09-25-c226.md | 1 + ...tral-conteudos-preview-frame-video-impl.md | 207 ++++++++++ ...nteudos-preview-frame-video-ui-design.html | 197 +++++++++ scripts/lib/e2e-affected-manifest.mjs | 5 + .../conteudos/[slug]/frame/route.ts | 98 +++++ .../conteudos/ContentPieceHomeCard.tsx | 5 + .../conteudos/ContentPieceMedia.tsx | 125 +++++- src/lib/contentPieceCatalog.ts | 41 +- src/lib/contentPieceFrame.ts | 108 +++++ src/lib/contentPieceHomeSelection.ts | 2 + src/utilities/content/contentPieceFrameJob.ts | 283 +++++++++++++ src/utilities/content/contentPieceJob.ts | 29 +- tests/e2e/fixtures/e2eTest.ts | 26 +- tests/e2e/frontend.e2e.spec.ts | 7 + tests/e2e/frontendConteudos.e2e.spec.ts | 69 +++- tests/fixtures/fake-ffmpeg.mjs | 10 + tests/int/contentPiece.int.spec.ts | 385 +++++++++++++++++- tests/unit/contentPieceCatalog.unit.spec.ts | 25 ++ tests/unit/contentPieceFrame.unit.spec.ts | 98 +++++ .../unit/contentPieceHomeBoard.unit.spec.tsx | 1 + .../contentPieceHomeSelection.unit.spec.ts | 22 + tests/unit/contentPieceMedia.unit.spec.tsx | 250 ++++++++++++ 22 files changed, 1963 insertions(+), 31 deletions(-) create mode 100644 docs/changelog/2026-09-25-c226.md create mode 100644 docs/plans/central-conteudos-preview-frame-video-impl.md create mode 100644 src/app/(frontend)/conteudos/[slug]/frame/route.ts create mode 100644 src/lib/contentPieceFrame.ts create mode 100644 src/utilities/content/contentPieceFrameJob.ts create mode 100644 tests/unit/contentPieceFrame.unit.spec.ts create mode 100644 tests/unit/contentPieceMedia.unit.spec.tsx diff --git a/docs/changelog/2026-09-25-c226.md b/docs/changelog/2026-09-25-c226.md new file mode 100644 index 00000000..f742a96d --- /dev/null +++ b/docs/changelog/2026-09-25-c226.md @@ -0,0 +1 @@ +**Frame estático para vídeos com arquivo na Central (C226, Issue #1352):** toda peça de vídeo publicada e com arquivo no acervo passa a exibir um frame estático leve no card público **antes** do play, sem nenhum request do arquivo de vídeo antes do gesto (`preload="none"` intacto, S27 preservado). O frame é um derivado privado na coleção `contentMedia` (nome determinístico `content-piece-frame-.jpg`), gerado por um dono só (`ensureContentPieceFrame`) chamado pelo job C211 — que já tem o arquivo em disco, então a extração sai de graça — e pela rota `GET /conteudos//frame` em miss, que cura o acervo anterior peça a peça, sem backfill, fila ou collection nova e **sem migration**. A porta reusa o gate de slug publicado e o proxy de mídia privada do `/midia`: o kill switch fecha as duas de uma vez. Sem frame possível (falha de extração, arquivo acima do cap de 256 MB, peça sem arquivo), o slot mostra o estado neutro honesto com play e o marcador "Frame indisponível" — nunca imagem quebrada, nunca retry. A projeção declara o frame por **elegibilidade** (vídeo com arquivo) e não pela existência do derivado, então a cura é invisível para o cache da listagem e não há tag para bustar; a mesma peça também ganha o frame na página da peça e no card da home (variante compacta de 124px, cena 05 do artefato). O guard de falha de e2e passou a aceitar `RegExp` e a checar os paths declarados antes do caso do favicon (todo 404 do browser traz a mesma mensagem do favicon). Débitos registrados: teto agregado da cura pública (#1356) e paridade de cromo pré-existente do slot (#1358). diff --git a/docs/plans/central-conteudos-preview-frame-video-impl.md b/docs/plans/central-conteudos-preview-frame-video-impl.md new file mode 100644 index 00000000..d065f9fc --- /dev/null +++ b/docs/plans/central-conteudos-preview-frame-video-impl.md @@ -0,0 +1,207 @@ +# Impl: C226 — Central de Conteúdos: frame estático para vídeos com arquivo + +Status: aprovado +Atualizado em: 2026-09-25 +Issue: #1352 +Intenção: docs/plans/central-conteudos-preview-frame-video.md +Appetite restante: herdado (~1–2 dias eng) — sem corte novo: o plano reusa o gate público do S27, o proxy de mídia privada do C193/C199, o `runFfmpeg` dono (C199), a linha privada `contentMedia` e o slot de mídia compartilhado; **sem migration, sem fila, sem backfill e sem collection nova**. Editor de frame, backfill, player novo e o acervo de falas (C182) seguem fora (intenção). + +> Modo `--auto` (work-issue): plano nasce aprovado pelo agente. Design UI do gate: `docs/plans/central-conteudos-preview-frame-video-ui-design.html` — **não redesenhar**. Trigger de designer declarado em **D5: non-trigger** — o markup aprovado (CENA 01/02 com frame, CENA 04 fallback neutro) recebe só o `` e o marcador; qualquer estado novo que a execução precise para além disso é **gatilho de designer antes do markup**, não improviso. + +## Leitura da intenção + +- **Outcome:** toda peça de vídeo publicada e com arquivo no acervo mostra, no catálogo público, pelo menos **um frame estático** antes do play — imagem leve, carregada como imagem, **sem nenhum request do arquivo de vídeo** antes do gesto; sem frame possível, o slot neutro desenhado com play honesto. +- **O que NÃO negociar:** + - **Play sob demanda:** nada de elemento de mídia antes do gesto, `preload="none"` preservado, o arquivo de vídeo só entra no play (S27). + - **Kill switch:** peça despublicada **não** expõe sua mídia privada pelo novo preview. Derivado em coleção pública é proibido; a porta pública do frame usa o mesmo gate de slug publicado do `/midia`. + - **Arquivo privado continua privado:** `ContentMedia` segue `canReadContentPiece`; o frame nunca entra em `media` (anonymous-read por contrato, `Media.ts:16-20`). + - **Falha isolada:** frame que falha **não** marca a peça `falhou`, não escreve `error`, não invalida `step`; e o card não tenta repetir a busca sem limite. + - **Sem frame de terceiro:** peça-link (Instagram/YouTube sem arquivo) continua sem frame local; thumbnail de plataforma não conta. + - **Superfícies:** alvo obrigatório é `/conteudos`; a lista interna `/campanha/comunicacao/conteudos` **não** ganha player nem segunda visualização de frame (o player dela é separado, `preload="metadata"`, e fica intocado). + - **C182 proibido:** não reusar nem alterar o preview do acervo de falas (`speechPoster*.ts`, rota `/acervo/[id]/poster`). + - **Escopo de produto:** sem backfill/reprocessamento em massa, sem editor de timestamp, sem analytics nova, sem Consent, sem alteração do kill switch. +- **O que reavaliar (hipóteses da intenção, confirmadas ou resolvidas no código):** + - "o card só monta `