From 1dd70b0647193a73848fb04fb665297aa274b7b3 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Sat, 26 Sep 2026 15:03:06 -0300 Subject: [PATCH] =?UTF-8?q?feat(C233):=20=C3=A1lbum=20p=C3=BAblico=20de=20?= =?UTF-8?q?fotos=20com=20busca=20por=20data,=20local,=20atividade=20e=20pe?= =?UTF-8?q?ssoa=20p=C3=BAblica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS-public.md | 8 +- DESIGN.md | 2 + docs/changelog/2026-09-26-c233.md | 1 + docs/plans/album-fotos-publico-busca-impl.md | 195 + .../album-fotos-publico-busca-ui-design.html | 630 +- playwright.config.ts | 10 + scripts/lib/e2e-affected-manifest.mjs | 35 +- src/app/(frontend)/(home)/cards/page.tsx | 10 +- src/app/(frontend)/(home)/page.tsx | 19 +- src/app/(frontend)/[type]/page.tsx | 8 +- .../(frontend)/conteudos/(catalog)/page.tsx | 16 +- src/app/(frontend)/conteudos/[slug]/page.tsx | 11 +- src/app/(frontend)/fotos/[id]/midia/route.ts | 73 + src/app/(frontend)/fotos/layout.tsx | 18 + src/app/(frontend)/fotos/page.tsx | 173 + src/app/(frontend)/jingles/page.tsx | 11 +- src/collections/ArchivePhoto.ts | 119 +- src/components/CampaignFooter.tsx | 20 +- src/components/CampaignLogoLink.tsx | 32 + .../conteudos/ContentPiecePageHeader.tsx | 33 +- src/components/fotos/ArchivePhotoCard.tsx | 59 + src/components/fotos/ArchivePhotoFilters.tsx | 192 + .../fotos/ArchivePhotoFiltersSheet.tsx | 196 + src/components/fotos/ArchivePhotoGrid.tsx | 73 + src/components/fotos/ArchivePhotoHero.tsx | 34 + src/components/fotos/ArchivePhotoLightbox.tsx | 174 + .../fotos/ArchivePhotoPageHeader.tsx | 40 + .../fotos/ArchivePhotoRemovalBand.tsx | 25 + src/components/fotos/ArchivePhotoStates.tsx | 72 + src/components/fotos/archivePhotoClasses.ts | 30 + .../shareLink/ShareLinkAnnouncement.tsx | 4 +- src/globals/PhotoAlbum.ts | 81 + src/lib/archivePhotoCatalog.ts | 52 +- src/lib/archivePhotoPublicCatalog.ts | 482 + src/lib/shareLink.ts | 1 + src/lib/slug.ts | 3 + ..._163248_add_archive_photo_publication.json | 20440 ++++++++++++++++ ...26_163248_add_archive_photo_publication.ts | 53 + src/migrations/index.ts | 6 + src/payload-types.ts | 47 + src/payload.config.ts | 11 +- .../archivePhotos/archivePhotoReads.ts | 75 + src/utilities/documents.ts | 3 + src/utilities/flickr/archivePhotoIngest.ts | 3 + .../privateMedia/privateMediaResponse.ts | 103 +- src/utilities/revalidateRequest.ts | 9 + tests/e2e/frontendFotos.e2e.spec.ts | 346 + tests/int/archivePhotoPublicRead.int.spec.ts | 277 + tests/unit/archivePhotoCatalog.unit.spec.ts | 37 + .../archivePhotoPublicCatalog.unit.spec.ts | 333 + tests/unit/campaignHome.unit.spec.tsx | 7 + tests/unit/e2eAffectedManifest.unit.spec.ts | 4 + tests/unit/shareLink.unit.spec.ts | 1 + 53 files changed, 24502 insertions(+), 195 deletions(-) create mode 100644 docs/changelog/2026-09-26-c233.md create mode 100644 docs/plans/album-fotos-publico-busca-impl.md create mode 100644 src/app/(frontend)/fotos/[id]/midia/route.ts create mode 100644 src/app/(frontend)/fotos/layout.tsx create mode 100644 src/app/(frontend)/fotos/page.tsx create mode 100644 src/components/CampaignLogoLink.tsx create mode 100644 src/components/fotos/ArchivePhotoCard.tsx create mode 100644 src/components/fotos/ArchivePhotoFilters.tsx create mode 100644 src/components/fotos/ArchivePhotoFiltersSheet.tsx create mode 100644 src/components/fotos/ArchivePhotoGrid.tsx create mode 100644 src/components/fotos/ArchivePhotoHero.tsx create mode 100644 src/components/fotos/ArchivePhotoLightbox.tsx create mode 100644 src/components/fotos/ArchivePhotoPageHeader.tsx create mode 100644 src/components/fotos/ArchivePhotoRemovalBand.tsx create mode 100644 src/components/fotos/ArchivePhotoStates.tsx create mode 100644 src/components/fotos/archivePhotoClasses.ts create mode 100644 src/globals/PhotoAlbum.ts create mode 100644 src/lib/archivePhotoPublicCatalog.ts create mode 100644 src/migrations/20260926_163248_add_archive_photo_publication.json create mode 100644 src/migrations/20260926_163248_add_archive_photo_publication.ts create mode 100644 src/utilities/archivePhotos/archivePhotoReads.ts create mode 100644 tests/e2e/frontendFotos.e2e.spec.ts create mode 100644 tests/int/archivePhotoPublicRead.int.spec.ts create mode 100644 tests/unit/archivePhotoPublicCatalog.unit.spec.ts diff --git a/AGENTS-public.md b/AGENTS-public.md index 9de940da..f494e5dc 100644 --- a/AGENTS-public.md +++ b/AGENTS-public.md @@ -1,5 +1,11 @@ # Public site domain (AGENTS layer — load when touching `(frontend)`) +## Photo album (`/fotos`) — C233 + +The public album over the private archive (`archivePhoto`, C231/C232): **only `publicationStatus: 'approved'` photos** are read (`getApprovedArchivePhotoItems`, `src/utilities/archivePhotos/archivePhotoReads.ts`), cached under the collection listing tag `archivePhotos` and busted by the collection's `afterChange`/`afterDelete` — an `approved→removed` edit pulls the photo and its media down **without a deploy**. The route canonicalizes to `/fotos`; the single-value facets are `data` (day, `YYYY-MM-DD`), `municipio` (catalog slug), `atividade` (an `ARCHIVE_PHOTO_SCENES` value), `pessoa` (slug of a canonical `publicFigureCatalog` name) plus `q`/`pagina`; `?foto=` is the non-canonical overlay and `/fotos/` keeps 404ing (no page per photo). Media goes through the same-origin proxy `/fotos//midia` (`?tamanho=grade` resizes on the fly, `?download=1` names `jorge-solla-1313-foto-.`, `X-Robots-Tag: noindex`); the municipality name/slug are snapshotted on the row (`municipalityName`/`municipalitySlug`) because the `municipality` collection is campaign-only. + +The album global `photoAlbum` holds the two product knobs: `published` (kill switch — **only `false` closes**; an absent row means the schema default `true`) and `removalChannelUrl` (the visible "é você nesta foto? peça a remoção" channel, never invented in code). Publishing the album or approving a photo **requires a valid channel** (fail-closed guard in the global and in the collection); the album has no `loading.tsx` on purpose — a Suspense boundary would turn the closed album's `notFound()` into a `200`. The "Fotos" discovery link in `CampaignFooter` appears while there are approved photos (and always on `/fotos` itself). + ## Posts & Tags (news / publications) The public news/publications system is backed by two collections (both in the `Publicações` admin group) plus the helpers in `src/utilities/posts.ts`. @@ -37,7 +43,7 @@ curl -X POST "https:///api/revalidate?tag=global_privacy-policy" \ - **Auth:** send the secret via `x-revalidate-secret` or `Authorization: Bearer `; it is compared to `REVALIDATE_SECRET` with a constant-time check. - `REVALIDATE_SECRET` must be set in the **production** env (and in your local env if you want to test the endpoint). It is documented in `.env.example`. -- **Tag:** optional `?tag=` query or JSON body `{ "tag": "..." }` (query wins). Allowlist: `posts` (default), `global_privacy-policy`, `election-tse` (bust after re-running `pnpm db:seed:tse` so the cached elections-tab reads refresh), `municipality-catalog` (bust after a migration/seed that adds or renames a município so chip indexes and `unstable_cache` loaders refresh), `social-feed` (bust after a direct-DB write to the `social-feed-settings` global — e.g. an admin-less seed of the YouTube/Instagram feed config; admin edits self-revalidate). Unknown tag → `400`. +- **Tag:** optional `?tag=` query or JSON body `{ "tag": "..." }` (query wins). Allowlist: `posts` (default), `global_privacy-policy`, `election-tse` (bust after re-running `pnpm db:seed:tse` so the cached elections-tab reads refresh), `municipality-catalog` (bust after a migration/seed that adds or renames a município so chip indexes and `unstable_cache` loaders refresh), `social-feed` (bust after a direct-DB write to the `social-feed-settings` global — e.g. an admin-less seed of the YouTube/Instagram feed config; admin edits self-revalidate), `archivePhotos` (bust after a direct-DB write to `archivePhoto` rows — admin edits self-revalidate via the collection hook; the public album `/fotos` caches under it). Unknown tag → `400`. - **Instagram sync status (S11):** the global's `instagramSyncStatus` jsonb (hidden) is written by the render path, by `afterChange` (re-sync only when IG credentials changed), and by the admin button `POST /api/social-feed/sync` (admin session via `payload.auth` + same-origin guard); the `InstagramSyncStatusPanel` ui field renders it. Runbook de credenciais: `docs/ops/instagram-feed-token-runbook.md`. - **Responses:** `200 { revalidated: true, tag: '...' }` on success; `401` if the secret is missing/wrong; `500` if `REVALIDATE_SECRET` is not configured on the server. - This POST is the required last step of the post-seed / direct-DB-change runbook. diff --git a/DESIGN.md b/DESIGN.md index 80c33677..63ad590d 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -290,6 +290,8 @@ Every `/campanha` list surface composes the shared pieces in `src/components/cam This file is the living owner of Teqo's visual decisions — a working document, not a snapshot. The old single-source-of-brand guardrail (kit 1313 as the only authority; "never re-derive lockups or palette") is retired. The official kit assets in `public/campaign-kit/` stay available — and their technical usage quirks still apply when used — but they no longer bind new design work. +**Public campaign archive actions (C233, 2026-09-26).** On campaign-branded public archive/read surfaces, the primary action is campaign red with white text. Kit yellow is an orientation and keyboard-focus signal — hero edge, restrained eyebrow, current-location underline, and high-contrast focus outline — never the competing CTA fill. This keeps one action dominant, separates public campaign expression from Petition Gold, and makes keyboard location unmistakable over both light and dark media surfaces. + How rules change: - **Decide, then update the owner.** When a better pattern wins — anti-slop review (`design-taste-frontend` / `gpt-taste`), a reference study (`awesome-design-md`), a redesign pass (`redesign-existing-projects`), or implementation judgment — update this file in the **same change** and say why. A pattern that only lives in code or in an agent's head is drift, not design. diff --git a/docs/changelog/2026-09-26-c233.md b/docs/changelog/2026-09-26-c233.md new file mode 100644 index 00000000..a31819f2 --- /dev/null +++ b/docs/changelog/2026-09-26-c233.md @@ -0,0 +1 @@ +**C233 — Álbum público de fotos com busca por data, local, atividade e pessoa pública (2026-09-26):** o acervo privado do C231/C232 ganha a porta pública `/fotos` — só a `publicationStatus: 'approved'` (novo select `draft|approved|removed`, default `draft`: a migration não publica nada), com o read anônimo cacheado em `src/utilities/archivePhotos/archivePhotoReads.ts` sob a tag `archivePhotos` (bustada pelo `afterChange`/`afterDelete` da collection: `approved→removed` some do índice e a mídia dela passa a 404 sem deploy) e o município denormalizado na própria linha (`municipalityName`/`municipalitySlug` gravados no hook do C232, que já lia a geografia com bypass documentado — backfill idempotente na migration `add_archive_photo_publication`, junto do global novo). O contrato puro `src/lib/archivePhotoPublicCatalog.ts` define a URL canônica (`/fotos`), as facetas single-value `data` (dia), `municipio` (slug do catálogo), `atividade` (cena do C232), `pessoa` (nome canônico do `publicFigureCatalog`), `q` e `pagina` (24/página, `pagina=1` dropado), o overlay não-canônico `?foto=` (sem página por foto) e o heading contextual (município→atividade→pessoa→data→termo). A mídia same-origin `/fotos//midia` reusa o proxy privado com `?tamanho=grade` (resize em streaming com sharp, sem variante armazenada — aposta do v1 com gatilho de revisita) e `?download=1` (`jorge-solla-1313-foto-.`), com `X-Robots-Tag: noindex` e o mesmo 404 para draft/removida/inexistente. O global `photoAlbum` (admin-only) segura o kill switch `published` (default `true` por decisão explícita do gate de 2026-09-26: abrir agora, sem esperar a eleição) e o `removalChannelUrl`; **publicar o álbum ou aprovar foto exige canal válido** (guard fail-closed no global e na collection, que só guarda a transição para `approved`), e a banda "é você nesta foto? peça a remoção" some sem canal. UI portada classe-a-classe do design estendido (header com nav desktop, hero sem CTA do C234, busca GET sem JS, chips + menus `
` no desktop e sheet com selects no mobile, grade 2/3 colunas com hover/foco, overlay com trap de Tab e foco devolvido ao card, dois vazios honestos e rodapé com "Fotos" condicional) e **`Design tier: openai/gpt-5.6-sol` com paridade certificada em 3 passadas** contra o app renderizado; o lockup S36 virou dono único `CampaignLogoLink` e o rodapé ganhou alvos de 44px. Doc do contrato em `AGENTS-public.md` (tag `archivePhotos` na allowlist). Testes: unit do contrato puro (parse/href/facetas/filtro/view model/paginação + vocabulário de publicação), int do read/guard/rota de mídia sobre `teqo_test`, e2e novo `frontendFotos` (manifest + curado + projeto; filtros, overlay, download, pull-down imediato e kill switch por HTTP real). Hard-stops aprovados na sessão (schema, URL público, bypasses, miniatura, timing); defers com gatilho no impl plan. Impl plan: `docs/plans/album-fotos-publico-busca-impl.md`. diff --git a/docs/plans/album-fotos-publico-busca-impl.md b/docs/plans/album-fotos-publico-busca-impl.md new file mode 100644 index 00000000..4c3d92c0 --- /dev/null +++ b/docs/plans/album-fotos-publico-busca-impl.md @@ -0,0 +1,195 @@ +# Impl: C233 — Álbum público de fotos com busca por data, local, atividade e pessoa pública + +Status: aprovado (modo `--auto`) — hard-stops aprovados explicitamente pelo humano na sessão (2026-09-26); timing eleitoral decidido: **abrir agora** +Atualizado em: 2026-09-26 +Issue: #1368 +Intenção: docs/plans/album-fotos-publico-busca.md +Design UI (gate): docs/plans/album-fotos-publico-busca-ui-design.html +Appetite restante: herdado (~1–2 dias eng) — **uma porta pública nova (`/fotos`) sobre o catálogo do C232**, sem segunda curadoria. Corte explícito para caber: capítulos/álbuns por evento viram heading derivado do filtro + ordenação; sem página própria por foto; miniatura on-the-fly (sem pipeline de variantes armazenadas); sem workflow de atendimento (staff marca `removed`); busca por selfie é C234. + +> Aprovado pelo próprio agente via `--auto` (modo autônomo do `work-issue`); a pausa do GATE virou apresentação-no-chat. Os hard-stops exigiram aprovação humana explícita e foram aprovados na sessão (`--auto` aprova o plano, não o schema nem o contrato público): schema/migration ✅, contrato `/fotos` ✅, bypasses intencionais ✅, miniatura on-the-fly ✅. **Timing eleitoral: o humano decidiu "implementar e abrir agora" (2026-09-26)** — sobrepõe a recomendação de pós-eleição do plano de intenção; o album nasce aberto (`photoAlbum.published` default `true`), com kill switch e pull-down imediatos. + +## Leitura da intenção + +- **Outcome:** o cidadão chega à foto certa por data, município, atividade, pessoa pública ou termo — e só vê o que a curadoria aprovou; toda foto mostra contexto real (data, local, atividade, quem aparece), tem vazio honesto e um canal visível de remoção cujo efeito é imediato e fail-closed. +- **O que NÃO negociar:** + - Só aprovadas aparecem; não aprovada não é acessível nem por URL direta (a mídia responde 404 igual). + - Pessoa pública só pelo catálogo curado (`publicFigureCatalog`), por nome canônico — nunca `Contact`, nunca rosto/biometria. + - Remoção a pedido: foto removida some do público **e do índice** sem deploy; sem registro humano não republica. + - Sem contador/ranking/analytics de vaidade; sem like/comentário/share com PII; imagens same-origin pelo proxy privado (bucket fechado). + - URL pública é contrato congelado; tag nova entra na allowlist fechada. +- **O que reavaliar (hipóteses da "Direção no codebase"):** + - "Só a leitura cacheada da lista inteira" — vale no v1 porque o conjunto **aprovado** é curadoria manual (começa vazio), com gatilho de revisita registrado (D2). + - "O read público resolve o município" — confirmado, mas **não** lendo a collection `municipality` (campaign-only): o hook existente denormaliza nome/slug na própria linha (D2). + - "A rota `/fotos` existe ao deployar" — risco eleitoral assumido por decisão do gate (D9): o v1 nasce **aberto** com kill switch global e pull-down por foto. + - "O design aprovado pode ser seguido como está" — não: o CTA "Encontre você nas fotos" é a busca por selfie (C234) e há heading/controles sem comportamento definido; o `designer` estende o artefato antes do markup (D8). + +## Abordagem recomendada + +```mermaid +flowchart LR + FICHA["/admin · Foto do acervo\npublicationStatus draft|approved|removed"] --> HOOK["beforeValidate\nderiveArchivePhotoCatalogIndex (C232)"] + HOOK --> MUNI["municipality (read-only geography)\nbypass documentado já existente"] + HOOK --> ROW[("archivePhoto\n+ municipalityName/Slug snapshot")] + ROW -- "afterChange / afterDelete" --> REV["revalidateArchivePhotosListing()\ntag archivePhotos"] + ROW --> READS["archivePhotoReads (unstable_cache)\nwhere approved · select explícito · depth 0"] + REV -. busta .-> READS + READS --> PURE["lib/archivePhotoPublicCatalog\nparse/href/facetas/filtro/view model"] + PURE --> PAGE["/fotos · kill switch global photoAlbum.published\n(?data&municipio&atividade&pessoa&q&pagina)"] + PAGE --> MEDIA["/fotos/[id]/midia\ngrade (sharp) · download"] + READS --> MEDIA + PAGE -. "?foto=id (não canônico)" .-> DET["overlay detalhe\nVoltar/Fechar + canal de remoção"] +``` + +**Opções consideradas (abordagem geral):** A) rota nova `/fotos` sobre o catálogo C232, com estado de publicação, read público cacheado por tag, contrato puro próprio, mídia same-origin e gate de timing por global; B) reusar `/conteudos` com uma faceta "foto"; C) construir álbuns/coleções curadas + página por foto. +**Recomendação:** **A** — é o contrato da intenção (URL `/fotos`, acervo ≠ peça editorial), reusa o dono de cada mecanismo (C232 para campos/vocabulário, `contentPieceReads` para o padrão de cache, `privateMediaResponse` para o I/O, `documents.ts`+allowlist para a tag) e cabe no appetite sem collection nova. +**Rejeitadas:** **B** porque mistura dois produtos e faria a Central carregar 6,5k fotos como "peças"; **C** porque exige segunda curadoria, página por foto e explosão de sitemap — os cortes literais da intenção. + +### Componentes / mudanças + +- **`src/lib/archivePhotoCatalog.ts`** (editar): vocabulário de publicação `ARCHIVE_PHOTO_PUBLICATION_STATUSES = ['draft','approved','removed']` + labels + `isArchivePhotoPublicationStatus` + predicado `archivePhotoIsPublic(record)` (espelho de `contentPieceIsPublic`); permanece a fonte de cenas/pessoas/datas. `publicationStatus` **não** entra em `ARCHIVE_PHOTO_CURATED_FIELDS` (`:61-86`) — é decisão editorial, não campo que o job disputa. +- **`src/lib/archivePhotoPublicCatalog.ts`** (novo, puro): `ARCHIVE_PHOTO_ALBUM_PATH='/fotos'`, `ARCHIVE_PHOTO_ALBUM_PAGE_SIZE=24`, `ARCHIVE_PHOTO_THUMB_WIDTH`, params/facetas/href canônico/parse fail-closed, `archivePhotoPublicPeople`, facetas derivadas, filtro AND+termo, heading derivado, labels de data (UTC) e view model (`mediaPath`, `thumbnailPath`, `downloadPath`). +- **`src/collections/ArchivePhoto.ts`** (editar): campo `publicationStatus` (select, `defaultValue:'draft'`, `required`, `index`), `municipalityName`/`municipalitySlug` (text, `readOnly`, slug `index`); `deriveArchivePhotoCatalogIndex` (`:150-211`) estende o `select` da leitura de município (`:183`) para `{ name: true, slug: true }` e grava os snapshots; `defaultColumns` (`:224`) ganha o status; hooks novos de revalidação (`afterChange`/`afterDelete`, espelho `ContentPiece.ts:92-106,271-272`). +- **`src/globals/PhotoAlbum.ts`** (novo): global `photoAlbum` (grupo `Configurações`), `read: () => true`, `update: payloadAdminOnly`, `published` (checkbox, default `true` por decisão do gate de 2026-09-26 — a rota nasce aberta; é o kill switch da superfície), `removalChannelUrl` (text, valida `https?://`/`mailto:`), hook `beforeValidate` fail-closed (publicar sem canal válido recusa) e `afterChange` → revalidar a tag do global. Registrar em `src/payload.config.ts` (`globals`, `:161`). +- **`src/utilities/archivePhotos/archivePhotoReads.ts`** (novo, `server-only`): `select` explícito (`id`, `alt`, `catalog`, `municipalityName`, `municipalitySlug`, `takenOn`, `searchText`, `filename`, `mimeType`), `where publicationStatus=approved`, `sort ['-takenOn','-id']`, `depth 0`, `unstable_cache` na tag da collection; `getApprovedArchivePhotoItems/ById` e `hasPublishedArchivePhotos` (descoberta). +- **`src/utilities/privateMedia/privateMediaResponse.ts`** (editar): novo `buildPrivateMediaImageResponse` — mesma abertura S3/disco de `:107-160`, mas quando há `width` faz o resize **em streaming** (`sharp(stream).resize(...)`) sem temp-file; sem `width`, delega ao `buildPrivateMediaResponse` (`:175-212`) que **já suporta** `download`/`dispositionFilename` (nenhuma mudança nele). +- **Routes:** `src/app/(frontend)/fotos/page.tsx` (sem `loading.tsx` de propósito: o boundary de Suspense flusha o shell antes da página e o `notFound()` do kill switch viraria `200` com a UI de not-found; o gate é checado no `generateMetadata` e na página), `src/app/(frontend)/fotos/[id]/midia/route.ts` (`force-dynamic`; `?tamanho=grade`, `?download=1`; `X-Robots-Tag: noindex`). +- **UI:** `src/components/fotos/` — `ArchivePhotoPageHeader`, `ArchivePhotoHero`, `ArchivePhotoFilters`, `ArchivePhotoCard/Grid`, `ArchivePhotoLightbox` (client mínimo), `ArchivePhotoStates`, `ArchivePhotoRemovalBand`; `src/components/CampaignFooter.tsx` ganha `showFotos` + `current='fotos'` (padrão dos flags existentes, `:27-95`). +- **Revalidação:** `src/utilities/documents.ts` ganha `revalidateArchivePhotosListing()` (usa o `revalidateTagSafely` de `:27-34`); `src/utilities/revalidateRequest.ts` ganha a tag `archivePhotos` na allowlist (`:16-26`) e `AGENTS-public.md` documenta a tag. +- **Migration:** `pnpm migrate:create add_archive_photo_publication` — aditiva: coluna `publication_status` (`default 'draft'`), colunas `municipality_name`/`municipality_slug`, tabela do global `photo_album`, **backfill** dos snapshots de município por `UPDATE ... FROM municipality` (SQL revisado à mão; nenhuma migration existente tocada). `pnpm generate:types`. +- **Testes/manifest:** ver D10. +- **Access / Consent:** nenhum `Consent` novo; nenhuma pessoa nova; `update` da collection e do global seguem `payloadAdminOnly`; read anônimo e `overrideAccess` da mídia reusam os precedentes documentados (hard-stop 4). + +### Dados → forma (se aplicável) + +N/A — a intenção declara: nenhum número, contador ou ranking na superfície; a lista é acervo com metadado editorial, não apresentação de dado (prompt 3 de data-presentation não se aplica). + +## Decisões de engenharia + +### D1 — Estado de publicação (schema + migration + backfill) (caro) + +**Opções:** A) checkbox `published`; B) select `publicationStatus: draft | approved | removed` (default `draft`, indexado); C) reusar `curatedFields`/`catalog.source` como marcador. +**Recomendação:** **B** — o aceite exige "remoção não republica": `removed` é estado sticky que o pipeline nunca escreve e que só a staff reverte; A não distingue "nunca curada" de "removida a pedido" e um clique re-publicaria. Backfill: a migration cria a coluna com default `draft` — **nenhuma** das fotos já catalogadas nasce pública; a assessoria aprova deliberadamente. A edição ocorre na mesma ficha do `/admin` (grupo Comunicação; `access.update = payloadAdminOnly`, `ArchivePhoto.ts:227-232`); abrir a curadoria para a sessão `communicator` é item separado (gatilho registrado). +**Rejeitadas:** **A** porque não preserva a remoção; **C** porque `curatedFields` arbitra job×curadoria de campos de catálogo e `source` é o resultado da pré-catalogação — categoria errada. + +### D2 — Read público cacheado e resolução do município (caro) + +**Opções:** A) módulo de domínio `src/utilities/archivePhotos/archivePhotoReads.ts` + snapshot `municipalityName/Slug` na linha (gravado pelo hook, backfill na migration); B) módulo top-level registrado no pin; C) ler a relação no caminho público (depth 1 ou query extra com bypass). +**Recomendação:** **A** — o hook **já** lê o município a cada save com o bypass documentado de geografia (`ArchivePhoto.ts:177-190`); denormalizar ali mantém o read público 100% dentro da própria linha (`depth 0`, `select` explícito, sem Flickr/geo/EXIF), sem novo bypass e filtrando `publicationStatus === 'approved'` **e** `archivePhotoIsPublic` no mapper (fail-closed em duas camadas). A subpasta `archivePhotos/` evita o pin top-level (`codebaseConventions.unit.spec.ts:421-573`); `flickr/` continua o dono da ingestão (fonte), não do álbum. Cache: `unstable_cache(['archive-photos'], { tags:[getCollectionListingTag('archivePhoto')] })`, espelhando `contentPieceReads.ts:26-65`; lista inteira das aprovadas filtrada em memória — **gatilho de revisita** quando as aprovadas passarem de ~1k (consultas por página + snapshot de facetas). +**Rejeitadas:** **B** (pin novo para uma leitura de domínio); **C** (o read de `municipality` é campaign-only — `municipalities.ts:192` — e um bypass extra no caminho público vaza superfície por dado que é imutável e o hook já tem em mãos). + +### D3 — Contrato de URL, facetas e paginação (caro) + +**Opções:** A) `data` (dia exato `YYYY-MM-DD`), `municipio` (slug do `municipalityCatalog`), `atividade` (valor de `ARCHIVE_PHOTO_SCENES`), `pessoa` (slug do nome canônico), `q`, `pagina` (1-based, canônico dropa 1) e `foto` (id numérico, **não** canônico) — puros em `src/lib/archivePhotoPublicCatalog.ts`; B) `mes`/`ano` + `tema`; C) multi-value por facet. +**Recomendação:** **A** — single-value no vocabulário existente, no padrão `contentPieceCatalog.ts:105-156`; o dia casa com `takenOn` (meio-dia UTC; parse reusa `archivePhotoTakenOn`, `archivePhotoCatalog.ts:256-271`) e é a pergunta real ("a foto da agenda"); as opções do controle de Data derivam **só** dos dias com foto aprovada (vocabulário fechado, não um calendário completo) e o controle (select × menu) é confirmado pelo `designer` em D8; `ARCHIVE_PHOTO_ALBUM_PAGE_SIZE = 24` mantém a grade leve (designer confirma 2/3 colunas); `buildArchivePhotoAlbumHref` serializa na ordem fixa `data,municipio,atividade,pessoa,q,pagina` e só inclui `foto` a pedido; a metadata canônica é `/fotos` sem filtros e `robots:noindex` quando zero aprovadas (espelho `page.tsx:45-81`); facetas derivam **só** dos itens aprovados (filtro que não casa não renderiza); `pessoa` casa `slugify(resolvePublicFigureName(nome) ?? nome)` (`publicFigureCatalog.ts:77-85`); `municipio` casa `municipalitySlug`; `atividade` só com cenas presentes, ordenadas pelo vocabulário. +**Rejeitadas:** **B** (mês/ano não é a pergunta e "capítulo" é heading derivado, não entidade; `tema` não existe neste acervo); **C** (quebra o contrato single-value do repo e faz a URL explodir com 8 pessoas). + +### D4 — Porta pública de mídia, miniatura e download (caro) + +**Opções:** A) `/fotos/[id]/midia` com `buildPrivateMediaResponse` para a imagem cheia e `?tamanho=grade` com resize on-the-fly (sharp, uma imagem, sem storage), `?download=1` para "Baixar foto"; B) `imageSizes` no upload + regeneração/backfill das aprovadas; C) servir o original em tudo. +**Recomendação:** **A** — o repo nunca usou `imageSizes` e as 6,5k fotos existentes não têm variantes; sharp já é dependência e o domínio já faz exatamente esse resize (`flickr/archivePhotoCatalog.ts:177-186`). O resize é **em streaming** (`sharp(stream)` sobre o objeto aberto do S3/disco, sem temp-file; sharp é o mesmo dono que o C232 já usa), dentro do `privateMediaResponse` (I/O num dono só), com os headers privados intocados (`no-store`, `privateMedia.ts:41`) e `X-Robots-Tag: noindex` nas duas variantes (evita 6,5k imagens indexáveis). O gate é o read cacheado (`getApprovedArchivePhotoById`) → 404 idêntico para draft/removida/inexistente; a rota re-lê a linha com o `overrideAccess` documentado do precedente (`conteudos/[slug]/midia/route.ts:40-51`) para o filename fresco; disposição `inline` para exibir e `attachment` no download (o `buildPrivateMediaResponse` já aceita `download` + `dispositionFilename`), nome legível por helper puro (`jorge-solla-1313-foto-.`). O `no-store` é deliberado: pull-down imediato (right to erasure) não convive com cache público de derivada. +**Rejeitadas:** **B** (pipeline/backfill novo para um acervo majoritariamente não-público e sem precedente de `imageSizes`); **C** (contraria o aceite "miniaturas" — original de vários MB por card). +**Risco assumido:** CPU do resize por request (24/página, sem cache) — medir no e2e local; gatilho de revisita: p95 da página acima do aceitável ou aprovadas >~1k ⇒ variante armazenada (decisão explícita, hard-stop 5). + +### D5 — Tag de revalidação, hook e allowlist (caro) + +**Opções:** A) tag `archivePhotos` **derivada** (`getCollectionListingTag('archivePhoto')`) + allowlist + `revalidateArchivePhotosListing()` + `afterChange`/`afterDelete` na collection (espelho `ContentPiece.ts:92-106`); B) tag literal `archive-photos`; C) sem hook (TTL/deploy). +**Recomendação:** **A** — uma edição de curadoria (inclusive `approved→removed`) busta a lista e o `byId` do overlay: a foto some do índice e a mídia dela passa a 404 **sem deploy**; o helper seguro já engole o `revalidateTag` fora de request (`documents.ts:27-34`), então o ingest CLI não quebra; `documents.ts` continua sem importar `@payload-config`. +**Rejeitadas:** **B** (duas fontes para o mesmo nome); **C** (contraria o aceite de pull-down imediato). + +### D6 — Detalhe da foto em contexto (caro) + +**Opções:** A) overlay server-rendered na própria `/fotos` via `?foto=` (não canônico), id fora das aprovadas → sem overlay (fail-closed), "Voltar ao álbum"/"Fechar" apontando para o href filtrado sem `foto`, com um shell client mínimo para foco inicial e Esc; B) rota interceptada `/fotos/[id]`; C) página própria `/fotos/[id]` noindex. +**Recomendação:** **A** — preserva o corte "foto avulsa não ganha página própria" (`/fotos/` segue 404) e o voltar/fechar é navegação real que funciona sem JS; o overlay reusa o mesmo item do read cacheado, sem segunda query, e mostra o contexto curado (Data, Local, Atividade, Quem aparece) + Baixar + canal de remoção. +**Rejeitadas:** **B** (maquinário de rota interceptada sem ganho e cria um path por foto); **C** (contraria o corte de sitemap/6,5k páginas). + +### D7 — Canal de remoção (caro) + +**Opções:** A) global `photoAlbum` com `removalChannelUrl` (http(s)/mailto validado) e links diretos/pré-preenchidos (`mailto:` com assunto + link da foto), sem formulário; staff marca `removed` na ficha; publicar exige canal válido; B) link para `/contato`; C) form/collection de pedidos com `Consent`. +**Recomendação:** **A** — zero captura de dado e zero `Consent` novo; o destino é dado editável no `/admin` (nunca inventado no código — o gate fornece e o hard-stop 4 registra); "atendimento registrado" = `publicationStatus: removed` na ficha, sem workflow; a banda de rodapé (cena 05) e o detalhe consomem o mesmo campo. +**Rejeitadas:** **B** (rota inexistente no site — inventaria uma página de contato); **C** (collection/Consent/PII novos, fora do appetite e do invariante). + +### D8 — UI e extensão do design (caro — trigger (b) do designer) + +**Opções:** A) port classe-a-classe com o `designer` estendendo o artefato **antes** do markup; B) implementar o artefato literal; C) redesenhar. +**Recomendação:** **A** — superfícies e estados: header page-local no molde `ContentPiecePageHeader`; hero; busca GET sem JS + chips `Data`/`Município`/`Atividade`/`Pessoa pública` com `
` e chip ativo × (padrão `ContentPieceFilters.tsx:95-196`); grade 2/3 colunas com `` (sem `next/image`: proxy `no-store` fora da allowlist — `next.config.mjs:9-30`; precedente `ContentPieceMedia.tsx:184-193`); card com título (`catalog.caption ?? alt`), meta `data · município · atividade` e linha "Quem aparece"; paginação anterior/próxima preservando facetas; overlay (D6); vazios honestos no molde `ContentPieceStates.tsx:19-72`; banda de remoção (cena 05) + `CampaignFooter`; a11y: `alt` curado, teclado, foco visível, contraste, mobile-first. O `designer` precisa entregar **antes do markup**: (i) remoção/substituição do CTA "Encontre você nas fotos" (depende do C234 — **não pode ser seguido como está**); (ii) comportamento do heading "Agenda em destaque" (helper derivado do filtro ativo); (iii) padrão mobile das facetas (bottom sheet × linha de `
`); (iv) foco/fechar do overlay; (v) confirmação do "Baixar foto" no v1; (vi) controle de Data (drop-down dos dias existentes × outro padrão — o artefato só mostra "Qualquer data"); (vii) controle de paginação (o artefato não desenha um). +**Rejeitadas:** **B** (o artefato contém um CTA impossível nesta fatia — seguir literal é defeito); **C** (design aprovado existe; redesign é desperdício). + +### D9 — Timing eleitoral (decidido pelo humano — abrir agora) + +**Opções:** A) rota fechada por padrão (`photoAlbum.published=false` ⇒ `/fotos` 404 + noindex) com flip pós-eleição; B) env `ARCHIVE_PHOTO_ALBUM_PUBLIC`; C) deferir o item; D) **abrir agora** — `published` default `true`, rota viva no merge, com kill switch global e remoção por foto. +**Decisão do gate (2026-09-26):** **D** — o humano decidiu explicitamente implementar e abrir agora, sobrepondo a recomendação (assumida) de pós-eleição do plano de intenção. Consequências registradas: a proteção contra conteúdo segue sendo a curadoria (nenhuma foto nasce aprovada) + o estado `removed` por foto + o kill switch do global (flip sem deploy) + `noindex` enquanto não houver aprovadas; o canal de remoção é pré-requisito para fotos irem ao público (guard do global). +**Rejeitadas:** **A** (preferida do plano de intenção, preterida pelo gate); **B** (flip exigiria deploy/restart, invisível para o produto e sem trilha); **C** (a intenção pede o item e o gate decidiu não adiar). + +### D10 — Verificação por camada (barato, mas explícito) + +**Opções:** A) unit do contrato puro + unit dos pins existentes + int do read/rota/hook/global + e2e novo `frontendFotos` no manifest, no projeto Playwright e no curado; B) só int; C) e2e de browser para tudo. +**Recomendação:** **A** — unit `tests/unit/archivePhotoPublicCatalog.unit.spec.ts` (parse fail-closed de cada param, href canônico preservando facetas e dropando `foto`/`pagina=1`, facetas só de aprovadas, filtro AND + termo + dia, view model/heading/labels de data, slice de página); estende `tests/unit/archivePhotoCatalog.unit.spec.ts` (vocabulário/predicado de publicação); int `tests/int/archivePhotoPublicRead.int.spec.ts` (aprovada aparece; draft/removida não; escrita de sistema não muda `removed`; município resolvido pelo snapshot; `getApprovedArchivePhotoById`; hook busta; guard do global; importa `GET` da rota de mídia — precedente `contentEvents.int.spec.ts:40` e `contentPiece.int.spec.ts:48-49` — para 200/404 e download); e2e `tests/e2e/frontendFotos.e2e.spec.ts` (serial, REST admin com `adminHeaders`; seeda foto por multipart + flipa o global; filtra por facetas/termo, abre overlay, mídia 200/404, `removed` some; restaura o global no `afterAll` e limpa as linhas). Pins: entrada nova em `scripts/lib/e2e-affected-manifest.mjs` (prefixos da rota/componentes/contrato/utilities/collection/global/documents), `frontendFotos` em `E2E_CURATED_SPECS` (`:24-54`, obrigatório: a migration torna o PR high-risk) e no pin `tests/unit/e2eAffectedManifest.unit.spec.ts:51-103`; projeto `frontendFotos` em `playwright.config.ts:170-210` (testMatch próprio + `dependencies: isProdMode ? [] : ['frontend']`); `src/components/CampaignFooter.tsx` no manifest ganha `frontendFotos`. `codebaseConventions.unit.spec.ts:421-573` intocado (subpasta de domínio). +**Rejeitadas:** **B** (o contrato HTTP público e o kill switch pedem prova real); **C** (browser para lógica pura é caro e frágil). + +## Fases verificáveis + +1. **Tracer / schema+server — quota ~45%:** vocabulário/predicado + campos da collection + snapshot de município no hook + global + migration com backfill + `pnpm generate:types`; contrato puro; read cacheado; tag/helper/allowlist e hooks; unit + int verdes antes de seguir. +2. **UI (dispatch do designer primeiro) — quota ~35%:** extensão do artefato (D8), componentes `src/components/fotos/*`, página + metadata + `loading`, rota de mídia (grade/download), overlay, `CampaignFooter`. +3. **Gates — quota ~20%:** `pnpm gate:fast`; `pnpm test:int`; e2e local `pnpm test:e2e --no-deps --project=frontendFotos`; manifest/curado/pin/projeto; `pnpm push`; no fechamento, changelog `docs/changelog/2026-09-26-c233.md` e `AGENTS-public.md` (tag nova na allowlist + contrato do álbum). + +## Rabbit holes / Não escopo (engenharia) + +- **Página própria por foto** (`/fotos/[id]` page, sitemap de 6,5k) — corte literal; o `/fotos/` continua 404. +- **Álbuns/coleções por evento com segunda curadoria** — heading derivado do filtro + ordenação; sem entidade nova. +- **Selfie/reconhecimento facial (C234), busca por rosto ou nome inferido** — fora; o CTA do design é removido/substituído. +- **Like/comentário/share com PII, contador de views, "mais vistas"** — vedados pela intenção. +- **`Contact`/collection de pessoas, upload público, edição de imagem** — proibidos. +- **`Consent` novo ou reuse de opt-in para remoção** — proibido; o canal é link, sem captura. +- **`imageSizes`/variantes armazenadas e scripts de regeneração** — v1 é on-the-fly; gatilho: CPU/tempo da grade. +- **Abrir a curadoria para `communicator` (access)** — fora; a ficha segue no `/admin` com `payloadAdminOnly`. +- **Ler `municipality` publicamente / `depth 1` no read** — rejeitado em D2. +- **`ALTER TYPE` de `curatedFields`** — morto; `publicationStatus` não é campo de catalogação. +- **Busca semântica/tema no álbum** — não existe vocabulário; `q` é literal sobre `searchText`. +- **Prefetch agressivo/client router cache nas páginas filtradas** — manter `prefetch` padrão do Link; sem infinite scroll. + +## Riscos e mitigação + +- **Original pesado vazando para a grade:** miniatura on-the-fly em streaming (sharp) + página de 24 + `loading="lazy"`; e2e/int conferem a variante; gatilho para variante armazenada se CPU incomodar. +- **Custo da leitura da lista inteira a cada bust:** só aprovadas entram no `where`; o bust acompanha edições de curadoria (baixa frequência); gatilho >~1k aprovadas. +- **Backfill dos snapshots de município incorreto:** SQL revisado à mão + int com município seedado e foto catalogada antes da migration; a migration é aditiva e nunca substitui as existentes. +- **Remoção que "volta":** pipeline nunca escreve `publicationStatus`; `removed` só sai por edição humana; int cobre escrita de sistema com `context` preservando o estado. +- **Canal de remoção vazio em produção:** guard do global recusa publicar sem canal válido; links só existem com o canal preenchido; e2e cobre o guard. +- **Superfície pública nova em período eleitoral:** risco assumido explicitamente pelo humano (2026-09-26, D9 — abrir agora); mitigado por curadoria (nada nasce aprovado), `removed` fail-closed, kill switch global e `noindex` enquanto vazio. +- **`revalidateTag` fora de request (CLI/e2e):** helper seguro existente; int cobre o write sem request. +- **S3 parcial/derrubada de boot:** guard existente de `mediaStorage` intocado; a nova rota usa o mesmo `resolvePrivateMediaStaticDir` de dev/test. +- **e2e com estado global compartilhado:** spec serial + `beforeAll`/`afterAll` restaurando `photoAlbum`; a suíte roda `fullyParallel` com 2 workers — mitigar não afirmando o link novo em specs alheios e revisar ao adicionar o projeto. +- **PR high-risk (migration):** curado com `frontendFotos` + pin do curado atualizado no mesmo PR; `pnpm test:e2e --no-deps --project=frontendFotos` local. + +## Hard-stops (aprovação humana explícita — valem mesmo em `--auto`) + +1. **Schema/migration:** `publicationStatus` + snapshots de município + global `photoAlbum` + backfill SQL — `pnpm migrate:create add_archive_photo_publication` e `pnpm migrate` só no banco local do worktree (`teqo_wt233`/`teqo_wt233_test`); aditiva, sem tocar migration existente; revisar o SQL; `pnpm generate:types`. **✅ Aprovada na sessão `--auto` (2026-09-26).** +2. **Contrato público/URL:** `/fotos` + params `data|municipio|atividade|pessoa|q|pagina|foto`, rota `/fotos/[id]/midia` (variantes grade/download), canonical `/fotos`, noindex quando vazio, e a tag `archivePhotos` entrando na allowlist fechada de `revalidateRequest.ts` (com atualização de `AGENTS-public.md`). **✅ Aprovado na sessão `--auto` (2026-09-26).** +3. **Timing eleitoral (produto): ✅ decidido pelo humano (2026-09-26) — abrir agora.** `photoAlbum.published` nasce `true`; a rota abre no merge/deploy. Nenhuma foto existente é aprovada pela migration; o canal de remoção é pré-requisito para aprovar fotos. +4. **Bypasses intencionais:** (a) leitura anônima da lista aprovada via Local API (padrão documentado de `contentPieceReads.ts:13-24`); (b) `overrideAccess: true` na rota de mídia só para o filename fresco (precedente `conteudos/[slug]/midia/route.ts:40-51`); (c) o bypass de geografia do hook C232 (`ArchivePhoto.ts:177-190`) é **estendido, não criado**. O destino do canal de remoção é dado de produto fornecido pelo gate — **nunca inventar endereço/URL no código**. **✅ Aprovados na sessão `--auto` (2026-09-26).** +5. **Miniatura on-the-fly:** decisão de performance deliberada do v1 (sharp em streaming por request, sem storage); se o gate de produto preferir variante armazenada, a troca é decisão explícita (afeta D4) antes do push. **✅ Aprovada na sessão `--auto` (2026-09-26).** + +## Adiado com gatilho (triagem do /simplify, 2026-09-26) + +Nenhum achado virou Issue nova (zero `expensive_lock` ≥4). Os baratos ficaram registrados aqui: + +- **Forma do nome de download (4ª cópia)** — `archivePhotoDownloadFilename` repete `contentPieceDownloadFilename`/jingle/webSpeech; já absorvido pelo defer do S27 (`escala-dry-pos-s27.md`). Gatilho: próximo toque em `src/lib/contentPieceCatalog.ts:571` ou `src/lib/jingle.ts` (unificar com a 4ª cópia). +- **`FACET_SLUG_PATTERN` residual** em `contentPieceCatalog.ts:99` e `shareLink.ts:27` (a C233 usa `SLUG_PATTERN` de `lib/slug.ts`). Gatilho: tocar qualquer um dos dois donos. +- **Chassi do bottom sheet** duplicado com `ContentPieceShareSheet` (portal + tema + Esc + backdrop). Gatilho: 3º overlay/sheet com o mesmo chassi. +- **`waitForSettledPage` duplicado** nos e2e (dono existente: `tests/e2e/fixtures/campaignE2EFixtures.ts` `waitForStreamSettled`; cópias pré-C233 em `frontendConteudos`/`campaignPeople`/`campaignConcepts`). Gatilho: próximo toque nos specs e2e. +- **`ArchivePhotoPublicSource` espelha o `select` à mão** + cast no read. Gatilho: `select` mudar sem o type acompanhar (derivar de `Pick`). +- **Sem trap de foco/`inert`** nos overlays (só foco inicial + Esc, como o `ContentPieceShareSheet`). Gatilho: requisito de a11y de modal pleno ou 3º overlay. +- **Acúmulo manual de chunks** no resize (`privateMediaResponse.ts`) em vez de `pipeline` + `toBuffer()`. Gatilho: próxima mexida no helper (avaliar com e2e da rota). +- **`RAISE NOTICE` do backfill** pode não aparecer no log do deploy. Gatilho: se o count não aparecer no 1º deploy real, trocar por log do migrator. +- **Resize on-the-fly sem cache** — decisão aprovada (hard-stop 5); revisita já condicionada a p95/>~1k aprovadas. + +## Aceite de engenharia + +- [ ] Aceite de produto coberto: busca por data/município/atividade/pessoa + termo; só aprovadas (nem por URL direta); contexto real; vazio honesto; canal de remoção visível com efeito imediato e `removed` não republicável pelo pipeline; cache por tag com pull-down imediato; a11y; grade leve mobile; same-origin/privado. +- [ ] Invariantes AGENTS/engineering-standards: sem cadastro paralelo/PII/`Consent`; `overrideAccess` só nos precedentes documentados; migração aditiva com SQL revisado; identificadores em inglês e copy pt-BR; nada de `next/image` em rota `no-store`. +- [ ] Testes de domínio previstos (unit/int) onde o read/gate/write mudam; manifest + curado + pin + projeto Playwright atualizados; e2e novo existe no disco. +- [ ] `pnpm gate:fast` verde; int do caminho verde; e2e `frontendFotos` verde local; `pnpm push` via GitHub; changelog + doc do contrato no fechamento. + +## Self-score + +**Self-score decision-quality: 4/5.** (1) todas as decisões caras (estado de publicação, read/denormalização, contrato de URL, mídia/miniatura, tag/hook, detalhe, remoção, timing, verificação) têm Opções + Recomendação + Rejeitadas; (2) cabe no appetite herdado: sem collection nova, uma migration aditiva com backfill, reusa o hook do C232, o padrão de read do S27, `privateMediaResponse`, o vocabulário de cenas/pessoas e o padrão de estados do catálogo; (3) rabbit holes nomeados (página por foto, álbuns, selfie, PII, `imageSizes`, access da curadoria, leitura pública de município, busca semântica); (4) depth check: donos existentes editados (collection/hook, documentos/tag, allowlist, footer, config do Playwright) e módulos novos só onde não há dono (contrato público, read do álbum, global, componentes `fotos/`); (5) outcome preservado — fail-closed, curadoria manda, sem PII, sem reescrever o aceite. O que impede o 5: a **miniatura on-the-fly** é aposta de performance não medida (CPU por request, sem storage) e o **timing eleitoral + o destino do canal de remoção** são decisões de produto (o timing foi decidido pelo humano em 2026-09-26; o destino do canal é dado do global, nunca inventado no código). diff --git a/docs/plans/album-fotos-publico-busca-ui-design.html b/docs/plans/album-fotos-publico-busca-ui-design.html index f1c1e314..5bcb51fa 100644 --- a/docs/plans/album-fotos-publico-busca-ui-design.html +++ b/docs/plans/album-fotos-publico-busca-ui-design.html @@ -51,6 +51,28 @@ outline: 3px solid var(--brand-yellow); outline-offset: 3px; } + .photo-card { + display: block; + border-radius: 12px; + color: inherit; + text-decoration: none; + transition: + transform 160ms ease, + box-shadow 160ms ease; + } + .photo-card:hover { + transform: translateY(-2px); + } + .photo-card:hover .photo-frame { + box-shadow: 0 12px 28px rgb(32 27 26 / 16%); + } + .photo-card:focus-visible { + outline: 3px solid var(--brand-yellow); + outline-offset: 4px; + } + .photo-frame { + transition: box-shadow 160ms ease; + } .photo-placeholder { position: relative; overflow: hidden; @@ -198,6 +220,9 @@ Rota pública · /fotos + Abre agora · acervo começa sem aprovações

Álbum público: encontrar a foto certa sem virar rede social @@ -206,6 +231,16 @@ Cidadão procura por termo, data, município, atividade ou pessoa pública; vê somente fotos aprovadas e entende o contexto antes de guardar a imagem.

+
+

Extensão do gate · 26 set 2026

+

+ O hero fica sem CTA: “Encontre você nas fotos” pressupunha selfie/reconhecimento facial + (C234), fora desta entrega. A busca logo abaixo já é a ação primária honesta; duplicá-la + com “Ver as fotos” criaria competição sem acrescentar destino. Data usa menu de dias + publicados no desktop e select no sheet mobile. O detalhe é sempre um dialog sobre a + grade, nunca uma página própria. +

+
@@ -253,11 +288,6 @@ data e contexto.

- Encontre você nas fotos
@@ -278,11 +308,37 @@
-
- +
+
+ +
+

+ Dias com fotos publicadas +

+ 12 de setembro de 2026 + 8 de setembro de 2026 + 3 de setembro de 2026 +
+
- @@ -418,11 +519,6 @@

Busque pela agenda, pelo lugar ou por quem apareceu.

- Encontre você nas fotos
@@ -437,40 +533,80 @@ >
+
+

+ Álbum público +

+

Fotos em Município

+
+
@@ -504,8 +640,14 @@ class="mt-1.5 h-11 w-full rounded-[10px] border border-black/20 bg-white px-3 font-normal" > + + + +

+ Somente dias que têm fotos publicadas. +