O escopo A/C do gate (PR #1370) é superado por decisão do dono (2026-10-01, aval jurídico declarado): o índice vira anônimo (archivePhotoFace, foto + descritor 128-d, sem nome/vínculo), o enrollment por pessoa morre (faceSubject/faces:enroll removidos) e qualquer visitante encontra as próprias fotos aprovadas.
- faces:index persiste os descritores por foto (transação substitui as linhas + marker = modelo); desaprovar/remover/deletar foto apaga os descritores (hooks + FK cascade)
- POST /api/fotos/selfie casa o vetor da consulta contra o índice em memória (TTL 60 s) e devolve só a interseção com o read aprovado do C233, sem score/nome de terceiro; leave-index apaga os descritores que casarem
- Consentimentos seguem fail-closed: busca-selfie-fotos (consulta) e busca-selfie-indice (aviso público do índice), textos versionados em scripts/lib/faceConsentTexts.mjs e provisionados por pnpm seed:face-consents
- pnpm archive:publish aprova o acervo em lote (draft -> approved; removed nunca) com guardas e recibo
- C243: seção condicional SelfieSearchHomeSection (port do design hi-fi, cenas 390/1280) entre a Central e a newsletter; sem engine/captura na home e sem CTA morto quando fechada
- docs: planos C242/C243, AGENTS-public, runbook §C242, changelog
Provas: gate:fast 457 arquivos/5.055 testes; int full 109/1.075; e2e frontendFotosSelfie 9/9; knip verde.
A seção S39 era só vitrine: quem via a peça do seu território tinha de ir
até o catálogo para compartilhar ou recortar. O card agora abre a MESMA
folha do S27 (mensagem de voto por tipo, contador C213, sem sair da home) e
as tags com faceta honesta — mais a fileira Tipo | Cidade | Região — levam
a /conteudos já filtrada pelo construtor canônico; nada filtra a amostra
local e as tags sem faceta (Mais recente, origem) continuam estáticas.
As facetas da fileira são derivadas no server (contentPieceCatalogFacets,
mesmo vocabulário do catálogo) e o lean ContentPieceHomeItem ganha só
sourceUrl, com a folha S27 estreitada ao que lê; classes novas são
home-scoped — CONTENT_PIECE_TAG e /conteudos ficam intactos. Sem migration,
Consent ou analytics novos.
Closes#1391
A lista /campanha/comunicacao/conteudos ganha um modo de seleção opt-in
que envolve os dois corpos (tabela e card list) com um estado client
compartilhado, chaveado pela URL canônica (a seleção morre em
página/filtro e nunca cresce para além da página visível). Com seleção
ativa, a barra flutuante aplica o mesmo veredito unitário à seleção:
Publicar/Despublicar agem direto com recibo honesto; Apagar sempre
confirma no CampaignDeleteDialog reusado (novo caminho onConfirm
aditivo), avisando os links públicos só quando a seleção carrega
publicadas.
O transporte é uma rota única (POST .../conteudos/lote,
campaignJsonMutationRoute, teto de 50 ids) que itera server-side as ações
donas: cada peça mantém a própria transação, o gate fresco e a limpeza
best-effort de mídia; a falha de uma nunca reverte as demais e o recibo
separa afetadas x falhas (2 de 3 peças apagadas. 1 falhou.).
Sem migration, sem Consent novo, sem rota pública (S27) e sem mudar a
semântica do C222. Cobertura: int do lote (papéis, dedupe, mídia, evento,
tag, falha parcial, schema), unit do builder/barra/seleção e e2e HTTP.
Design tier: DEGRADED (artefato de origem sem certificação frontier) —
crítica do designer aplicada no escopo do item; entrega bloqueada para
sign-off humano antes do PR. Related #1388
O diálogo "Enviar gravação" mandava o arquivo inteiro num único XHR e a
produção vive atrás do Cloudflare Tunnel, cujo edge responde 413 para
qualquer request body acima de ~100 MB ANTES do app ver o corpo (probe
read-only: POST de 150 MB cortado após ~3,2 MB). Um .mp4 de 6 GB de
plenária nunca chegava à rota — o C199-large havia removido só o teto do
app, não o do proxy.
O upload agora é fatiado ponta a ponta: o start bodyless cria a row e a
sessão em disco (`recording-upload-<id>` com estado + arquivo anexado), o
diálogo envia partes de 16 MiB em ordem estrita com progresso agregado e
o último byte do tamanho declarado finaliza a mídia privada e o job. Cada
parte faz heartbeat do `updatedAt` contra o reaper de 1 h, o start faz
preflight de disco (statfs + margem), falha de parte aborta a sessão e o
delete do usuário/reaper limpam o diretório. Sem migration e sem schema.
Regressão: protocolo int nas rotas start/chunk/abort (multi-parte, fora
de ordem, tamanho excedido, seam multipart, falha S3, reaper), guarda
unit do orçamento do edge, guardas e2e das rotas. Post-mortem e entrada
de changelog em docs/.
- /fotos/encontre + POST /api/fotos/selfie: só o vetor 128-d, sem imagem, sem score e
sem nomes de terceiros; fail-closed pela flag photoAlbum.selfieSearchEnabled (default
false) e pelos Consents por chave estável (busca-selfie-fotos/busca-selfie-indice, não
seedados — a superfície nasce fechada até o aval jurídico/DPIA).
- Índice A/C do gate (PR #1370): só quem aderiu é indexado; descriptors do acervo são
transitórios. Collection faceSubject (admin-only, sem vínculo com Contact), vínculos
matchedPhotos e marker faces.checkedKey no archivePhoto (modelo + enrolledAt).
- Engine @vladmandic/face-api nos dois lados: browser lazy com modelos same-origin
(copy-face-vision-assets no prebuild/predev/Docker) e CLI node-wasm sem tfjs-node.
CLIs ops-only faces:enroll / faces:index --apply|--verify com guardas C231/C232.
- Opt-out self-service por reconhecimento (leave-index) + canal de remoção do C233;
rate limit próprio (namespace selfie:<hash>) e bounded body extraído do beacon.
- UI portada classe-a-classe do design estendido (paridade certificada, tier primário);
e2e frontendFotosSelfie (manifest+curado+projeto, engine stubada) e canário do engine
real; runbook §C234, contrato em AGENTS-public.md e changelog.
- Migration aditiva add_face_subject (SQL revisado); hard-stops aprovados na sessão.
Troca o motor do modo tema (Câmara + internet) de expansão lexical para
embeddings (Deep Infra, BAAI/bge-m3), com índice próprio (speechEmbedding),
ranking por proximidade em memória, evidência real do trecho mais próximo,
vazio honesto por limiar e degradação para a busca literal. Inclui a
collection oculta + migration, o CLI pnpm acervo:index (--dry-run/--limit/
--source/--force/--probe), o port do design aprovado e a suíte de testes.
Pós-rebase: o snapshot da migration seguinte (20260926_062039_add_archive_photo,
do C231 já na main) foi regenerado para incluir speech_embedding — sem ele o
próximo migrate:create recriaria a tabela.
Closes#1360
Falha pré-existente reproduzida em origin/main: a página de demanda monta o
form de responsáveis duas vezes no DOM (visível + cópia oculta) e
campaignDemandVisibility batia nos dois com locator estrito — classe
"locator não-escopado" do plano OPS83/#824. O CI curado do PR roda a spec
(diff high-risk) e ficaria vermelho; os locators agora miram o elemento
visível.
Registro no impl plan do C231 e no changelog.
O conjunto curado roda specs concorrentes que publicam peças; a home pede o
still e /frame responde 404 até a cura (C226). As specs da home passam a usar
a mesma declaração que frontend.e2e.spec.ts, fechando o flake do CI.
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/<slug>/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
Hero com pattern oficial, placa do 1313 e faixa amarela; 1313 visivel no mobile. Filtros com ChevronDown do lucide centralizado (nao mais o glifo baixo) e accordion exclusivo via <details name>. Artefato: docs/plans/central-conteudos-hero-polimento-ui-design.html
Hard delete por peça (sem lixeira/lote/telemetria) em qualquer estado, com confirmação que nomeia o link público só quando publicado; access próprio da vertical + admin (advisor/leader fail-closed), cascata da mídia pós-commit e bust da tag contentPieces no afterDelete; máquina do diálogo extraída no 3º call site (C183/C199/C222).
No Orion/Safari os seis tiles de modelos colapsavam (só os rótulos
ficavam): a caixa do CardModelTile tirava a altura de um `aspect-ratio`
inline sendo filha de um flex com `overflow: hidden` — o construto do
bug WebKit #265243 (fix só no WebKit main 2026-05), invisível para o CI
Chromium-only.
O tile passa a tirar a altura de um espaçador em fluxo com
padding-bottom percentual (data-card-tile-ratio). Guardas: unit por
modelo (RED sem o fix), sweep de convenção nos cards e e2e de geometria
após relayout forçado. Post-mortem + changelog na mesma entrega.
Classificação automática com proveniência (job + CLI recordings:classify), facetas ano/tema/alcance/município/duração na URL canônica, busca por tema no corpus próprio, ordenação e a barra de filtros portada do design aprovado sobre os shells compartilhados. Deep-links existentes byte-idênticos; Câmara intocada.
Collection contentEvent (hidden, sem hooks, sem PII) + beacon público
POST /api/content-events fail-soft, download contado no servidor na rota
de mídia, agregação em uma query group-by e a coluna "Circulação" na lista
interna + painel na ficha (port classe-a-classe do artefato estendido,
crítica final do designer certificada).
O fallback de imagem OG (própria → global `metadata` → null, sempre absoluto
no deployment origin) deixa de estar copiado em 6–7 páginas públicas e ganha
o dono server-only `src/utilities/ogImageReads.ts` (`resolveOgImage(configured)
→ { url, media }`), com `resolveShareLinkOgImageUrl` delegando (sem twin) e os
8 consumidores migrados — 2 do S27, jingles, corte, abaixo-assinado ×2, artigo
e layout. `seo.ts` segue puro; sem migration, sem Consent/access, Impeccable A.
- unit novo do resolver (8 casos) + pin do módulo em codebaseConventions; int
do shareLink verde sem tocar asserção; e2e frontendConteudos/frontendJingles/
campaignSpeechCut/frontendShareLink/frontend verdes, com asserts novos de
og:image (helper `metaContent` extraído do spec do share-link)
- dono novo entra no E2E_AFFECTED_MANIFEST nas 4 entradas de OG públicas
- deltas intencionais registrados: deployment origin no lugar do canônico em
corte/abaixo-assinado/artigo e OG absoluto no layout
- shareLink ganha modo (direct|announcement), pool de destinos com "Destino no
ar" por linha e dados do evento; destination escalar sai via migration com
backfill lossless (legado vira direct com destino no ar)
- página de anúncio pública portada classe-a-classe, header campaign-site
extraído (CampaignPageHeader) e ativação/troca sem reload por poll da rota
/api/share-link/<slug>/live (fail-closed, no-store)
- agenda Google (action=TEMPLATE) + .ics por rota sob o slug; compartilhar o
link curto (kind event); S19 intacto e 404 uniforme preservado
Related #1268
/conteudos + /conteudos/<slug>: catálogo filtrável (Tipo · Cidade · Região ·
Tema · Instituição + termo na URL canônica), play sob demanda (Vídeo/Áudio só
carregam no toque; Foto com preview leve), Baixar e Compartilhar do próprio
card com a mensagem de voto por tipo (wa.me sem destinatário, editável; "A
mídia" usa a folha nativa e cai em download onde o aparelho não suportar) e o
card personalizado como peça sintética do catálogo → /cards.
- leitura pública nova (contentPieceReads) cacheada na tag contentPieces com o
gate where status=publicado + contentPieceIsPublic; a rota GET
/conteudos/<slug>/midia reusa buildPrivateMediaResponse (range, disposition,
private no-store, 404 uniforme) com download jorge-solla-1313-<slug>.<ext> e
a allowlist inline do dono estendida (webp/gif/avif + áudios das peças)
- contrato público puro do catálogo (filtros/VM/download) + mensagens de voto
(contentPieceShare); OG com fallback do global; conteudos entra nos slugs
reservados; kill switch por peça vale na hora (lista, página, rodapé/selo);
descoberta condicional no CampaignFooter e no header local
- UI portada classe-a-classe do design aprovado (cenas 01–08, com as cenas
06–08 estendidas no trigger a); sem migration, sem Consent e sem access novo;
contentMedia segue privada (sem segunda cópia de mídia)
- testes: 2 unit specs novos, 18 casos int (leitura pública + rota de mídia) e
e2e frontendConteudos (5 casos); manifest e curado atualizados
- follow-up de engenharia: #1276 (docs/plans/escala-dry-pos-s27.md)
Closes#1255
Envio em lote (1 request raw-body por arquivo, fan-out ≤2, até 50 arquivos) e
adicionar por link (Instagram/YouTube; extração só pela Graph API da conta
própria, senão peça-link) em /campanha/comunicacao/conteudos. Cada peça tem
estado próprio (Processando/Pronto/Falhou + Reprocessar), transcrição/texto e
catalogação automática revisável; publicar/despublicar preserva arquivo, slug
e publishedAt.
- collections contentPiece + contentMedia (upload privado no S3) e migration
20260923_032714_add_content_piece (slug único gerado no 1º publish,
sourceUrl única, searchText com índice trigram)
- pipeline reusa C199 (job after() + reaper + retry condicional, transcrição
DeepInfra, ffmpeg/chunk), o classificador de facetas do acervo e o
institutionCatalog; curatedFields impede o job de sobrescrever curadoria
- gate único canReadCommunicationCatalog (advisor/leader negados); delete
só pelo admin; sem Consent novo
- UI portada do design aprovado (lista desktop + cards mobile, diálogo de
lote com progresso por arquivo, diálogo de link, ficha editável, kill
switch); nav/chrome/column-picker/manifest atualizados
- instagramFeed expõe mediaUrl; revalidateTag ganha wrapper seguro fora de
request (fixtures/scripts)
- testes: 2 unit specs, 1 int spec (16 casos) e 7 e2e na vertical
- follow-up de engenharia: #1267 (docs/plans/escala-dry-pos-c211.md)
Closes#1254
O recorte do card 'Time de você' falhava em navegadores endurecidos (IronFox/Tor
desligam WebAssembly e WebGL por padrão) com a copy errada de 'tente outra foto';
o motor MediaPipe exige ambos (o grafo pede kGpuService mesmo com delegate CPU).
Agora o adapter sonda WASM/WebGL antes do download e devolve 'unsupported' com
mensagem acionável; a entrada de inferência é capada em 1600 e a foto passa pelo
loadCardPhoto (foto de 61MP que falhava agora conclui).
O enquadramento automático volta ao S15 (bbox cobre a janela): o S18 mirava a
caixa do rosto em 103px e deixava o busto inteiro pequeno na janela. Saem o
detector de rosto (asset removido), frameCardPhotoOnFace, a referência 103px, o
piso 0.2 e a âncora S20 (inerte sob o cover). S16/S17 intactos.