From eebe81cd1e664a4a194f5b205861ed30bddebd13 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Wed, 19 Aug 2026 17:23:41 -0300 Subject: [PATCH] =?UTF-8?q?feat(S8):=20se=C3=A7=C3=A3o=20'Nossa=20hist?= =?UTF-8?q?=C3=B3ria'=20na=20home=20=E2=80=94=20v=C3=ADdeo=20da=20trajet?= =?UTF-8?q?=C3=B3ria=20embutido=20do=20YouTube=20(Closes=20#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + docs/CHANGELOG-AGENTS.md | 2 + docs/changelog/2026-08-19-s8.md | 1 + docs/plans/secao-video-historia-home-impl.md | 159 +++++++++++++++++++ src/app/(frontend)/(home)/page.tsx | 2 + src/components/CampaignStorySection.tsx | 67 ++++++++ tests/e2e/fixtures/e2eTest.ts | 5 + tests/e2e/frontend.e2e.spec.ts | 78 +++++++++ 8 files changed, 317 insertions(+) create mode 100644 docs/changelog/2026-08-19-s8.md create mode 100644 docs/plans/secao-video-historia-home-impl.md create mode 100644 src/components/CampaignStorySection.tsx diff --git a/.gitignore b/.gitignore index ece4e785..6487dfde 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,6 @@ next-env.d.ts # Proprietary design font files kept only in the local workspace /public/arial-mt/ + +/data/encode/ +/data/poster-candidates/ diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index 7af7dbae..d941c67c 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -22,6 +22,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente **S7** — polimento dos cards da seção "Acompanhe de perto" da home de campanha (jorgesolla1313.com.br), look editorial e direto no lugar do "card de app": cantos retos (**sem `rounded-xl` no card e `rounded-lg` na capa**), capa de **borda a borda** (o `p-3` saiu do contêiner do card e foi para o bloco de texto — título/subtítulo/meta continuam respirando `p-3` das bordas; `mt-auto` da meta e `overflow-hidden` do zoom hover intactos) e, por decisão do humano posterior ao gate, **sem borda (`border --campaign-line`) e sem sombra** (`shadow-sm`/`hover:shadow-md`) — o card branco lê contra a banda do S5 só pela forma. **Chips de badge removidos de todos os cards** (decisão B do gate): nem os tipos de post do site (`POST_TYPE_BADGE_LABELS` deletado de `src/utilities/posts.ts`, junto com o describe no unit test) nem as origens externas YouTube/Instagram (sinalização externa segue por `target="_blank"`/`rel="noopener"`); `badgeLabel` saiu do tipo `CampaignContentCardData`. Superfície única no componente dono (`CampaignContentCard.tsx`) — bento e carrossel herdam; `ContentShareButton` (S4), banda do S5, carrossel e fail-closed da seção intocados. Zero migration/schema/Consent. e2e do describe da seção atualizados para o novo estado: loop de badges → ausência (`toHaveCount(0)` para os 4 tipos + YouTube/Instagram), pins CSS do card (`border-radius`/`border-width`/`box-shadow` + raio da capa) e pin geométrico da capa de borda a borda (boundingBox do card × capa). Verificação: gate:fast verde, unit 2415 + int 791 verdes, e2e frontend 19/19 local, format/knip/cycles limpos. Plano de implementação: `docs/plans/cards-acompanhe-de-perto-polimento-impl.md`. +**S8** — seção "Nossa história" na home de campanha (jorgesolla1313.com.br): o vídeo da trajetória do Dep. Jorge Solla (~1:40) ganha lugar próprio na home, após a seção "Bandeiras" e antes do rodapé — eyebrow "Nossa história" + título "Conheça a trajetória de Jorge Solla" + frame vertical 9:16 (desktop 2 colunas, copy à esquerda e frame 300px à direita centralizados; mobile empilhado, frame full-width cap 420px). **Divergência aprovada em execução pelo humano:** o vídeo é embutido do canal do deputado no YouTube (Short `i_fbclWWC5o`) em vez do asset self-hosted do plano de intenção — cada play embutido conta view no canal; `youtube-nocookie` (LGPD), `loading="lazy"` nativo (zero bytes até o frame se aproximar do viewport), `playsinline`, `allowfullscreen`, `title` a11y, `allow` com `compute-pressure` (silencia o console.error do player — o guard do e2e falha em console.error); CTA "Assistir à história" rola até o player (play cross-origin não é disparável). Os encodes one-off H.264/AV1 + poster da foto JOA00327 (14.9 MB, validados por design-vision contra o orçamento de 15 MB da intenção) ficam como artefato local gitignored em `data/encode/` — fallback pronto se um dia se voltar a self-hosted. Zero schema/migration/Consent; nenhuma seção existente tocada. e2e novo (posição, copy, atributos do embed, CTA→viewport, layout desktop 2 colunas com centro vertical, 9:16 mobile sem overflow) + abort global do `youtube-nocookie` no fixture `e2eTest.ts` (hermeticidade CI). Verificação: unit 2416 + int 791 verdes, tsc/lint/format/cycles/build verdes; e2e frontend 20/20; knip = limitação P3 pré-existente. /simplify: 3 revisores paralelos absorvidos (JSDoc enxuto, e2e sem assert redundante, plano sem conteúdo morto, desktop asserts); 2 débitos deferidos no impl plan (rótulo do CTA se a assessoria reabrir copy; dedupe da classe do CTA com `CampaignContentSection` quando S5 #33 terminar). + **OPS69** — o admin do Payload abria **em branco em produção** (P0, 2026-08-19): o `importMap` commitado (`src/app/(payload)/admin/importMap.js`) ficou sem a entrada `@payloadcms/storage-s3/client#S3ClientUploadHandler` — o commit OPS52 (`dabc00f4`) removeu o handler do Vercel Blob e **não** adicionou o do S3, porque o `pnpm generate:importmap` rodou **sem** as envs `S3_*`: com o plugin `s3Storage` condicional, sem as envs o importMap sai consistente (dev/test, storage local) e com elas (produção, Garage) o admin precisa do handler e o montava → `getFromImportMap: PayloadComponent not found in importMap` → login nunca renderizava. Fix mínimo (opção A do plano, aprovado no gate): regenerar o importMap com as 4 `S3_*` dummy inline (a mecânica do CLI: `iterateConfig` sem DB; `loadEnvConfig` do `@next/env` não sobrescreve envs já no processo) → diff de **exatamente +2 linhas** (import + entrada do `S3ClientUploadHandler`), nenhum outro drift. Verificação: smoke headless com S3 ativo no dev local — `/admin/login` renderiza (6 inputs, formulário visível, **zero erros de console**) vs. o branco reproduzido no diagnóstico; gates completos verdes (tsc/lint/format/knip/cycles, unit+int 791, build). Sem migration/access/Consent/UI. Docs: linha no AGENTS.md (checklist local: `generate:importmap` precisa das `S3_*` setadas — valores dummy ok — enquanto o plugin for condicional; sem elas o importMap fica órfão, classe OPS69) + item sucessor registrado (guard de CI que regenera com envs dummy e falha o gate se o arquivo commitado divergir — `depends` #77). Verificação pós-deploy: `/admin` abre o login e o log do container não re-emite o `getFromImportMap` para o handler (runbook `docs/ops/teqo-1313-deploy.md`). **OPS62-FOLLOWUP** — flake do `municipalitiesWithoutUpdateTool.unit.spec.ts` (2 ocorrências 2026-08-18: gate local 19:22 e CI run 779 23:53) eliminado pela raiz, com a hipótese original da Issue refutada por reprodução. Causa raiz: o fixture do teste (não o ambiente nem locale/ICU): `daysAgo()` chamava `Date.now()` **por chamada**; as duas construções `daysAgo(40)` de `Beta`/`Alpha` podiam cair em milissegundos diferentes sob carga (GC/preempção do fork do vitest na máquina compartilhada), deixando o timestamp de `Alpha` estritamente mais novo — em `sortStale` (`getMunicipalitiesWithoutUpdate.ts`) o sort por timestamp vencia o tie-break por nome e o teste recebia `[Beta, Alpha]` no lugar de `[Alpha, Beta]` (só o par trocado, resto correto — "sort não atuou" estava errado; o sort atuou sobre timestamps acidentalmente desiguais). Repro mecânica (sort de produção verbatim + fixture): fixture atual 0.08% de falhas em 200k runs sem pausa e **100%** com pausa de 1 ms injetada; fixture ancorado 0 em 2000 runs. Fix (test-only, nada em produção — o sort real é determinístico sobre timestamps fixos do DB): âncora `fixtureNowMs = Date.now()` no load do módulo (padrão do `prioritiesTool.unit.spec.ts`) + hoist do timestamp empatado (`const stale40 = daysAgo(40)` compartilhado por `Beta`/`Alpha` — igualdade estrutural, imune até a regressão da âncora) + comentário explicando a classe de flake. Scan da mesma classe: `prioritiesTool` (âncora no load) e `suggestionCatalog` (now fixo) já seguros; nenhum outro spec combina `Date.now()` por chamada com assunção de igualdade. Verificação: 30/30 runs do spec, unit full (249 arquivos/2405 testes), int (89/791), tsc, lint, format, check:cycles verdes (knip só com o P3 ledgered de sempre). /simplify: 3 reviewers aprovaram; débito absorvido no próprio teste (comentário do porquê). Impl plan: `docs/plans/ops62-followup-ci-flake-fixture-timestamps-impl.md`. diff --git a/docs/changelog/2026-08-19-s8.md b/docs/changelog/2026-08-19-s8.md new file mode 100644 index 00000000..d881378a --- /dev/null +++ b/docs/changelog/2026-08-19-s8.md @@ -0,0 +1 @@ +**S8** — seção "Nossa história" na home de campanha (jorgesolla1313.com.br): o vídeo da trajetória do Dep. Jorge Solla (~1:40) ganha lugar próprio na home, após a seção "Bandeiras" e antes do rodapé — eyebrow "Nossa história" + título "Conheça a trajetória de Jorge Solla" + frame vertical 9:16 (desktop 2 colunas, copy à esquerda e frame 300px à direita centralizados; mobile empilhado, frame full-width cap 420px). **Divergência aprovada em execução pelo humano:** o vídeo é embutido do canal do deputado no YouTube (Short `i_fbclWWC5o`) em vez do asset self-hosted do plano de intenção — cada play embutido conta view no canal; `youtube-nocookie` (LGPD), `loading="lazy"` nativo (zero bytes até o frame se aproximar do viewport), `playsinline`, `allowfullscreen`, `title` a11y, `allow` com `compute-pressure` (silencia o console.error do player — o guard do e2e falha em console.error); CTA "Assistir à história" rola até o player (play cross-origin não é disparável). Os encodes one-off H.264/AV1 + poster da foto JOA00327 (14.9 MB, validados por design-vision contra o orçamento de 15 MB da intenção) ficam como artefato local gitignored em `data/encode/` — fallback pronto se um dia se voltar a self-hosted. Zero schema/migration/Consent; nenhuma seção existente tocada. e2e novo (posição, copy, atributos do embed, CTA→viewport, layout desktop 2 colunas com centro vertical, 9:16 mobile sem overflow) + abort global do `youtube-nocookie` no fixture `e2eTest.ts` (hermeticidade CI). Verificação: unit 2416 + int 791 verdes, tsc/lint/format/cycles/build verdes; e2e frontend 20/20; knip = limitação P3 pré-existente. /simplify: 3 revisores paralelos absorvidos (JSDoc enxuto, e2e sem assert redundante, plano sem conteúdo morto, desktop asserts); 2 débitos deferidos no impl plan (rótulo do CTA se a assessoria reabrir copy; dedupe da classe do CTA com `CampaignContentSection` quando S5 #33 terminar). diff --git a/docs/plans/secao-video-historia-home-impl.md b/docs/plans/secao-video-historia-home-impl.md new file mode 100644 index 00000000..1d54d499 --- /dev/null +++ b/docs/plans/secao-video-historia-home-impl.md @@ -0,0 +1,159 @@ +# Impl: Seção de vídeo da história na home de campanha (S8) + +Status: em execução +Atualizado em: 2026-08-19 +Issue: #91 +Intenção: docs/plans/secao-video-historia-home.md +Appetite restante: herdado (~1 dia eng) + +## Divergência de produto aprovada pelo humano (2026-08-19) + +**Embed do YouTube no lugar de self-hosted.** O plano de intenção previa asset próprio +(WebM/AV1 + MP4/H.264 em `public/videos/`, lazy total, poster extraído/fornecido). Durante a +execução o humano constatou que o vídeo já está publicado no canal do deputado +(https://www.youtube.com/shorts/i_fbclWWC5o — "18 de agosto de 2026", confirmado pelo dono do +produto como o vídeo da história; a assessoria usa títulos datados) e **decidiu embutir o +YouTube**, divergindo do aceite "sem dependência de terceiros". Motivação do produto: cada play +embutido conta como view no canal (alimenta o algoritmo; o canal já é vitrine na seção +"Acompanhe de perto"). Aceite atualizado: + +- Seção com o vídeo da história via **iframe `youtube-nocookie`** (LGPD), `playsinline`, + `loading="lazy"` nativo (iframe só é buscado perto do viewport), frame 9:16, `allowFullScreen`, + título a11y. +- **Deixa de valer:** lazy-until-click total, poster próprio, chip de duração, orçamento de + ~15 MB de assets no repo (nada é commitado; os encodes one-off ficam em `data/encode/`, + gitignored, como artefato local caso se volte a self-hosted). +- **Mantém-se:** posição após "Bandeiras", cenas desktop (2 colunas) e mobile (empilhado) do + rascunho, CTA "Assistir à história" (agora rola até o player), zero schema/Consent, seções + existentes intactas. + +## Leitura da intenção + +Outcome e aceite da intenção (docs/plans/secao-video-historia-home.md), com a divergência +aprovada abaixo: seção com o vídeo da história na home pública, posição após "Bandeiras", +cenas desktop/mobile do rascunho aprovado, zero schema/migration/Consent, seções existentes +intactas, original de 236 MB fora do repo. O que permanece NÃO negociável: copy/pt-BR e +identificadores em inglês; lazy (nada pesado baixa sem o usuário pedir — o `loading="lazy"` do +iframe cobre isso); sem autoplay forçado; seções existentes intocadas. + +## Abordagem recomendada + +```mermaid +flowchart LR + YT["YouTube Short i_fbclWWC5o (canal do Solla)"] --> EMB["iframe youtube-nocookie (9:16, lazy, playsinline)"] + EMB --> SEC["CampaignStorySection (client island)"] + SEC --> HOME["src/app/(frontend)/(home)/page.tsx — após seção bandeiras"] +``` + +**Opções consideradas:** A) iframe do YouTube (decisão do humano) | B) `