From f05ddc2a672cade9ab06a66dbfd475b940ee3663 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Wed, 19 Aug 2026 16:13:31 -0300 Subject: [PATCH 1/3] feat(S7): cards 'Acompanhe de perto' sem raio, borda, sombra ou chip; capa de borda a borda (Closes #90) --- ...cards-acompanhe-de-perto-polimento-impl.md | 68 +++++++++++++++++++ src/components/CampaignContentCard.tsx | 23 ++----- src/components/CampaignContentSection.tsx | 4 -- src/utilities/posts.ts | 8 --- tests/e2e/frontend.e2e.spec.ts | 19 ++++-- tests/unit/postsDateFormats.unit.spec.ts | 13 +--- 6 files changed, 90 insertions(+), 45 deletions(-) create mode 100644 docs/plans/cards-acompanhe-de-perto-polimento-impl.md diff --git a/docs/plans/cards-acompanhe-de-perto-polimento-impl.md b/docs/plans/cards-acompanhe-de-perto-polimento-impl.md new file mode 100644 index 00000000..30d15bef --- /dev/null +++ b/docs/plans/cards-acompanhe-de-perto-polimento-impl.md @@ -0,0 +1,68 @@ +# Impl: Cards da seção "Acompanhe de perto": sem cantos arredondados, imagem de borda a borda, chip removido + +Status: aprovado +Atualizado em: 2026-08-19 +Issue: #90 +Intenção: docs/plans/cards-acompanhe-de-perto-polimento.md +Appetite restante: herdado (~0,5–1 dia; na prática, horas) + +## Leitura da intenção + +- **Outcome:** os cards da seção "Acompanhe de perto" (bento 1+4 no desktop, carrossel no mobile) passam a ter cantos retos (card e capa), capa de borda a borda, **sem borda e sem sombra no card** (decisão do humano em 2026-08-19, após o gate — estende o rascunho, que mantinha `border`/`shadow`) e nenhum chip de badge — nem tipos de post do site, nem origens externas (decisão B do gate). O texto interno (título, subtítulo, meta) continua com o respiro atual (`p-3`) em relação às bordas. +- **O que NÃO negociar:** nada além da superfície muda — compartilhamento (S4) usável no canto, grade do bento, carrossel, links, bandas do S5 (borda/background da seção) e o fail-closed da seção (some inteira quando nada é visível). Nada de tokens de cor, capas reais, proporções ou outras seções. +- **O que reavaliar:** a hipótese da intenção ("mover o padding para o bloco de texto e remover o badge; dados de badge tendem a ficar órfãos — o executor decide o que descartar"). Confirmado no código: `badgeLabel` vive só no card + nas três funções de mapeamento da seção + `POST_TYPE_BADGE_LABELS` (com um unit test dedicado). Decisão de engenharia: remover os órfãos por completo (tipo do card, constante e teste unitário) — manter morto seria dead code flagrável pelo knip (CI-blocking). + +## Abordagem recomendada + +```mermaid +flowchart LR + A[CampaignContentCard.tsx
superfície do card] --> B["card: -rounded-xl -p-3 -gap
capa: -rounded-lg
texto: +p-3
badge span: removido"] + A --> C["CampaignContentCardData
-badgeLabel"] + D[CampaignContentSection.tsx] --> E["-badgeLabel nas 3 funções
-import POST_TYPE_BADGE_LABELS"] + F[posts.ts] --> G["-POST_TYPE_BADGE_LABELS"] + H[postsDateFormats.unit.spec.ts] --> I["-describe POST_TYPE_BADGE_LABELS"] + J[frontend.e2e.spec.ts] --> K["badge loop: 812-814,
YouTube: 952,
Instagram: 1153
→ ausência + raio 0"] +``` + +**Opções consideradas:** A (superfície no card único + remoção dos órfãos) | B (variante/flag de card, ex. `variant="flat"`) | C (manter `POST_TYPE_BADGE_LABELS` e o badge no DOM com CSS oculto) +**Recomendação:** A — o card é o único componente da seção (bento e carrossel reusam `CampaignContentCard`); não há outro consumidor com cantos arredondados para preservar, então uma variante seria abstração sem dono. A remoção dos dados de badge é limpa: `badgeLabel` não é usado em mais nenhum lugar do repo. +**Rejeitadas:** B (over-engineering: nenhum outro card usa o componente; "edit the owner, don't twin"); C (dead code + dead data path; knip derruba o CI e o teste unitário continuaria pinando um contrato morto). + +### Componentes / mudanças + +- **`CampaignContentCard.tsx`** (`src/components/`): `cardClassName` vira constante sem `featured` (o único uso do param era o `gap-3`/`gap-2`; `featured` continua dirigindo `sizes` da imagem e o `h3`): `flex h-full flex-col bg-white transition-shadow ...` — sem `rounded-xl`, sem `p-3`, sem `gap`, **sem `border border-(--campaign-line)` e sem `shadow-sm`/`hover:shadow-md`/`transition-shadow`**. Capa: remove `rounded-lg` (mantém `overflow-hidden` — o zoom hover continua contido na própria capa). Bloco de texto: ganha `p-3` (`flex flex-1 flex-col gap-1 p-3`), igual ao draft. Badge `` removido; `badgeLabel` sai de `CampaignContentCardData`. O anel de foco (`focus-visible:ring-2 ring-(--pt-red)`) do link permanece — a11y. +- **`CampaignContentSection.tsx`** (`src/components/`): `badgeLabel` removido de `toArticleCardData`/`toVideoCardData`/`toInstagramCardData`; import `POST_TYPE_BADGE_LABELS` removido. Nada mais muda (limites, mesclagem, capas, metas). +- **`src/utilities/posts.ts`**: deleta `POST_TYPE_BADGE_LABELS` (único consumidor era a seção; `POST_TYPE_LABELS` plural fica — usado nas listagens). +- **`tests/unit/postsDateFormats.unit.spec.ts`**: import e describe de `POST_TYPE_BADGE_LABELS` removidos. +- **Migration:** sem migration (nenhuma mudança de schema/coleção). +- **Access / Consent:** nenhum. +- **UI:** Impeccable B — encaixe em um componente existente (shape→craft→critique→polish). O botão `ContentShareButton` (S4) segue `absolute top-2 right-2` sobre a capa, agora sem moldura; o draft mantém o botão no canto — sem mudança nele. + +### Dados → forma (se aplicável) + +- N/A — nenhum dado novo; apenas remoção de um campo de superfície (`badgeLabel`) do tipo de dados do card. + +## Fases verificáveis + +1. **Superfície + órfãos** — `CampaignContentCard.tsx`, `CampaignContentSection.tsx`, `posts.ts`, unit test do badge; verificação: `pnpm typecheck` + `pnpm test:unit` (o describe removido some, o resto passa). +2. **e2e** — `tests/e2e/frontend.e2e.spec.ts`: loop de badges (812–814) e as asserções `YouTube`/`Instagram` (952, 1153) → asserções de ausência (`toHaveCount(0)` para os quatro tipos + YouTube/Instagram nos testes de feed) e pin do novo visual no teste do bento (`border-radius: 0px` no card e na capa, `border-top-width: 0px` e `box-shadow: none` no card, no mesmo espírito dos pins CSS do S5). `toHaveCSS` com `first()` para não quebrar com o carrossel escondido no mobile. +3. **Gates** — `pnpm lint`, `pnpm format:check`, `pnpm exec knip`, `pnpm check:cycles`, `pnpm test` (unit+int). E2e roda no CI (OPS59) — local opcional com `pnpm test:e2e:affected`. + +## Rabbit holes / Não escopo (engenharia) + +- Não tocar `ContentShareButton`, `CampaignContentCarousel`, banda/bordas da seção (S5), `POST_TYPE_LABELS` (plural), `getVisiblePosts`/fail-closed ou capas. O rascunho HTML do gate fica intocado como artefato — a remoção de borda/sombra é decisão do humano posterior ao gate e não altera o arquivo do rascunho. +- Não criar variante de card, não esconder badge via CSS, não renomear `h3`/metas. +- Cuidado com o `gap` removido: o `mt-auto` da meta continua pinando a data ao rodapé do bloco; o `p-3` do bloco de texto é quem dá o respiro (draft). + +## Riscos e mitigação + +- **e2e que assere badge que não mapeei** — verificado por grep: só 812–814 (loop), 952 (YouTube) e 1153 (Instagram) no `frontend.e2e.spec.ts`; `waitForHomeHTML` só usa títulos/links de cabeçalho. Grep final antes do push. +- **Raio 0 pinado pelo e2e pode flake por arredondamento** — `toHaveCSS('border-radius', '0px')` é determinístico (nenhum token de raio no card/capa). +- **Dead code residual** — `POST_TYPE_BADGE_LABELS` e `badgeLabel` removidos do mesmo PR; knip no gate confirma. +- **Capa sem cover (placeholder `--campaign-band`)** — continua com borda a borda e cantos retos, igual ao draft; nada quebra. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção ainda coberto (raio 0 card+capa, capa borda a borda, texto `p-3`, nenhum chip em nenhum card, **sem borda e sem sombra no card**) +- [ ] Invariantes AGENTS/engineering-standards (card único como dono da superfície; S5/bandas intocadas; fail-closed da seção intacto) +- [ ] Testes de domínio previstos (unit/int) onde access/write paths mudam — N/A (nenhum write path muda); e2e atualizados para o novo estado visual diff --git a/src/components/CampaignContentCard.tsx b/src/components/CampaignContentCard.tsx index b51ce76b..8571b793 100644 --- a/src/components/CampaignContentCard.tsx +++ b/src/components/CampaignContentCard.tsx @@ -7,7 +7,6 @@ export type CampaignContentCardData = { id: string | number href: string title: string - badgeLabel: string meta: string /** Source of the content — drives the WhatsApp share message template. */ shareKind: ContentShareKind @@ -25,10 +24,8 @@ type CampaignContentCardProps = { featured?: boolean } -const cardClassName = (featured: boolean) => - `flex h-full flex-col rounded-xl border border-(--campaign-line) bg-white p-3 shadow-sm transition-shadow hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--pt-red) ${ - featured ? 'gap-3' : 'gap-2' - }` +const cardClassName = + 'flex h-full flex-col bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--pt-red)' /** * Presentational content card for the campaign home content bento/carousel — @@ -43,7 +40,7 @@ export const CampaignContentCard = ({ card, featured = false }: CampaignContentC const inner = ( <>
@@ -56,11 +53,8 @@ export const CampaignContentCard = ({ card, featured = false }: CampaignContentC className="object-cover transition-transform duration-300 group-hover:scale-[1.03] motion-reduce:transition-none motion-reduce:group-hover:scale-100" /> ) : null} - - {card.badgeLabel} -
-
+

{card.external ? ( - + {inner} ) : ( - + {inner} )} diff --git a/src/components/CampaignContentSection.tsx b/src/components/CampaignContentSection.tsx index f0c0e259..4fe90bf6 100644 --- a/src/components/CampaignContentSection.tsx +++ b/src/components/CampaignContentSection.tsx @@ -6,7 +6,6 @@ import { getCategoryName, getPostCanonicalPath, getVisiblePosts, - POST_TYPE_BADGE_LABELS, } from '@/utilities/posts' import { getInstagramFeed, type InstagramPost } from '@/utilities/socialFeed/instagramFeed' import { @@ -47,7 +46,6 @@ const toArticleCardData = (post: Post): DatedCard | null => { id: post.id, href, title: post.title, - badgeLabel: POST_TYPE_BADGE_LABELS[post.type], shareKind: 'article' as const, meta: [formatRelativePostDate(post.publishedDate), categoryName].filter(Boolean).join(' · '), ...(cover?.url ? { coverUrl: cover.url, coverAlt: cover.alt ?? undefined } : {}), @@ -62,7 +60,6 @@ const toVideoCardData = (video: YouTubeVideo): DatedCard => ({ id: `yt:${video.id}`, href: `https://www.youtube.com/watch?v=${video.id}`, title: video.title, - badgeLabel: 'YouTube', shareKind: 'video' as const, meta: [ formatRelativePostDate(video.publishedAt), @@ -81,7 +78,6 @@ const toInstagramCardData = (post: InstagramPost): DatedCard => ({ id: `ig:${post.id}`, href: post.permalink, title: post.caption ?? 'Publicação no Instagram', - badgeLabel: 'Instagram', shareKind: 'instagram' as const, meta: formatRelativePostDate(post.timestamp) ?? '', coverAspect: 'square', diff --git a/src/utilities/posts.ts b/src/utilities/posts.ts index 49307764..5a02869f 100644 --- a/src/utilities/posts.ts +++ b/src/utilities/posts.ts @@ -17,14 +17,6 @@ export const POST_TYPE_LABELS: Record = { evento: 'Eventos', } -/** Singular labels for per-card badges (e.g. the campaign home content bento). */ -export const POST_TYPE_BADGE_LABELS: Record = { - noticia: 'Notícia', - campanha: 'Campanha', - artigo: 'Artigo', - evento: 'Evento', -} - export const isPostType = (value: string): value is PostType => (POST_TYPES as readonly string[]).includes(value) diff --git a/tests/e2e/frontend.e2e.spec.ts b/tests/e2e/frontend.e2e.spec.ts index 342441ef..b60608cb 100644 --- a/tests/e2e/frontend.e2e.spec.ts +++ b/tests/e2e/frontend.e2e.spec.ts @@ -809,8 +809,10 @@ test.describe('Campaign home content section', () => { 'a[href^="/artigo/"], a[href^="/noticia/"], a[href^="/campanha/"], a[href^="/evento/"]', ) await expect(bentoCards.filter({ visible: true })).toHaveCount(5) - for (const badge of ['Artigo', 'Notícia', 'Campanha', 'Evento']) { - await expect(section.getByText(badge, { exact: true }).first()).toBeVisible() + // S7 — cards are badge-free: no type chip (site or external source) + // anywhere in the section. + for (const badge of ['Artigo', 'Notícia', 'Campanha', 'Evento', 'YouTube', 'Instagram']) { + await expect(section.getByText(badge, { exact: true })).toHaveCount(0) } await expect(section.getByText('E2e Artigo em destaque').first()).toBeVisible() await expect(section.getByText('E2e Conteúdo oculto')).toHaveCount(0) @@ -835,6 +837,13 @@ test.describe('Campaign home content section', () => { await expect(section).toHaveCSS('border-top-color', 'rgba(0, 0, 0, 0.12)') await expect(firstCard).toHaveCSS('background-color', 'rgb(255, 255, 255)') + // S7 — flat editorial cards: no radius, border or shadow (the white card + // reads against the band on shape alone), cover edge to edge. + await expect(firstCard).toHaveCSS('border-radius', '0px') + await expect(firstCard).toHaveCSS('border-top-width', '0px') + await expect(firstCard).toHaveCSS('box-shadow', 'none') + await expect(firstCard.locator('.aspect-video')).toHaveCSS('border-radius', '0px') + await featured.click() await expect(page).toHaveURL(new RegExp(`${featuredHref}$`)) await page.goBack() @@ -949,7 +958,8 @@ test.describe('Campaign home content section', () => { await expect( section.getByText('há 30 minutos · 12,4 mil visualizações').first(), ).toBeVisible() - await expect(section.getByText('YouTube', { exact: true }).first()).toBeVisible() + // S7 — no external-source chip on YouTube cards anymore. + await expect(section.getByText('YouTube', { exact: true })).toHaveCount(0) await expect(section.getByText('E2e Vídeo excluído')).toHaveCount(0) await expect(section.getByRole('link', { name: /E2e Artigo no mix/ }).first()).toBeVisible() @@ -1150,7 +1160,8 @@ test.describe('Campaign home content section', () => { await expect(featured).toHaveAttribute('href', 'https://www.instagram.com/p/e2e-ig-muro-1/') await expect(featured).toHaveAttribute('target', '_blank') await expect(featured).toHaveAttribute('rel', 'noopener noreferrer') - await expect(section.getByText('Instagram', { exact: true }).first()).toBeVisible() + // S7 — no external-source chip on Instagram cards anymore. + await expect(section.getByText('Instagram', { exact: true })).toHaveCount(0) await expect(section.getByText('há 20 minutos').first()).toBeVisible() await expect(section.getByText('E2e Post de grade')).toHaveCount(0) diff --git a/tests/unit/postsDateFormats.unit.spec.ts b/tests/unit/postsDateFormats.unit.spec.ts index 6f763389..994a88c7 100644 --- a/tests/unit/postsDateFormats.unit.spec.ts +++ b/tests/unit/postsDateFormats.unit.spec.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { formatRelativePostDate, POST_TYPE_BADGE_LABELS } from '@/utilities/posts' +import { formatRelativePostDate } from '@/utilities/posts' describe('formatRelativePostDate', () => { afterEach(() => { @@ -40,14 +40,3 @@ describe('formatRelativePostDate', () => { expect(formatRelativePostDate('2026-07-17T12:00:00Z')).toBe('17 de julho de 2026') }) }) - -describe('POST_TYPE_BADGE_LABELS', () => { - it('spells the four types in the singular for card badges', () => { - expect(POST_TYPE_BADGE_LABELS).toEqual({ - noticia: 'Notícia', - campanha: 'Campanha', - artigo: 'Artigo', - evento: 'Evento', - }) - }) -}) -- 2.45.2 From 60b2d665eb4d5803b1ea8602485d1bcbfd0f9315 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Wed, 19 Aug 2026 16:19:03 -0300 Subject: [PATCH 2/3] =?UTF-8?q?test(S7):=20pins=20e2e=20endurecidos=20?= =?UTF-8?q?=E2=80=94=20contrato=20de=20borda=20completo=20e=20capa=20de=20?= =?UTF-8?q?borda=20a=20borda=20medida?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/e2e/frontend.e2e.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/e2e/frontend.e2e.spec.ts b/tests/e2e/frontend.e2e.spec.ts index b60608cb..df9e0454 100644 --- a/tests/e2e/frontend.e2e.spec.ts +++ b/tests/e2e/frontend.e2e.spec.ts @@ -840,9 +840,15 @@ test.describe('Campaign home content section', () => { // S7 — flat editorial cards: no radius, border or shadow (the white card // reads against the band on shape alone), cover edge to edge. await expect(firstCard).toHaveCSS('border-radius', '0px') - await expect(firstCard).toHaveCSS('border-top-width', '0px') + await expect(firstCard).toHaveCSS('border-width', '0px') await expect(firstCard).toHaveCSS('box-shadow', 'none') await expect(firstCard.locator('.aspect-video')).toHaveCSS('border-radius', '0px') + // Edge to edge: the cover spans the full card width (no container padding). + const cardBox = await firstCard.boundingBox() + const coverBox = await firstCard.locator('.aspect-video').boundingBox() + expect(coverBox).not.toBeNull() + expect(cardBox).not.toBeNull() + expect(Math.abs(coverBox!.width - cardBox!.width)).toBeLessThanOrEqual(1) await featured.click() await expect(page).toHaveURL(new RegExp(`${featuredHref}$`)) -- 2.45.2 From c2af46dc1c8d40a321a153994296061248d93798 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Wed, 19 Aug 2026 16:19:35 -0300 Subject: [PATCH 3/3] chore(S7): entrada no changelog agregado (OPS44) --- docs/CHANGELOG-AGENTS.md | 2 ++ docs/changelog/2026-08-19-s7.md | 1 + 2 files changed, 3 insertions(+) create mode 100644 docs/changelog/2026-08-19-s7.md diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index 856a6460..1cebc11b 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -4,6 +4,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`. + **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-s7.md b/docs/changelog/2026-08-19-s7.md new file mode 100644 index 00000000..dc72862b --- /dev/null +++ b/docs/changelog/2026-08-19-s7.md @@ -0,0 +1 @@ +**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`. -- 2.45.2