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
5.8 KiB
Escala e DRY pós-C211 (Central de Conteúdos)
Status: rascunho
Atualizado em: 2026-09-23
Issue: #1267 (depends: [1254])
Item do roadmap: — (follow-up de engenharia da entrega C211)
Responsável: —
Appetite: ~0,5–1 dia eng (fill-in; três fases pequenas, sem migration)
Impeccable: A — só backend/refactor
Contexto
A entrega C211 (central-conteudos-ingestao.md, Issue #1254) criou o pipeline de peças reusando deliberadamente os mecanismos do C199 (job after() + reaper + retry condicional, mídia privada, transcrição, classificador de facetas). A passagem /simplify (2 revisores, 0 achados altos) aplicou ~18 correções na própria sessão — revalidateTag do listing na collection, passo morto no job, narrowing de relação, texto arquivando o arquivo, filename no anexo, mensagens de limite alcançáveis, SetCampaignPageChrome, labels pt-BR dos campos curados, guard de anexo no util, pin do chrome, entre outras. O que sobrou são débitos maiores que o cleanup da sessão e do mesmo lote (mesma superfície: src/utilities/content/ + testes novos). Sem este item: (a) o guard de stream com teto e o staticDirOf da mídia privada passam a ter 3ª/4ª cópia (o dono src/utilities/privateMedia/ já existe); (b) os select da peça e o ponto do reaper ficam re-escritos entre action/loader/rotas; (c) a marcação de campos curados na action carrega generalidade morta; (d) asserts fracos nos specs novos deixam regressões passarem.
Objetivos
- O guard de stream com teto (raw body e resposta HTTP) e o
staticDirOfda mídia privada vivem num dono único emsrc/utilities/privateMedia/(ou no upload owner), reusados por C199/C211/speech sem mudar teto, mensagens ou comportamento. contentPieceSelect/selects do loader da peça vivem num módulo do domínio e o ponto dereapStaleContentPieceé único (hoje: status action; o loader é read-only desde a sessão).- A marcação de
curatedFieldsna action da ficha perde a tupla+loop (o form envia todos os campos;title/typesão obrigatórios no schema). - Asserts dos specs de C211 apertados: valores exatos de mídia/arquivo onde hoje há
toBeTruthy(); sem round-trip no-op no e2e. - Guardrails: sem migration; sem mudança de comportamento visível;
overrideAccess/transação inalterados;pnpm gate:fast+ int + e2e da vertical verdes.
Fases
- F1 — dono do stream/staticDir de mídia privada (score 3, maior ROI). Extrair
streamBodyToFile/streamResponseToFile/staticDirOfpara o dono (src/utilities/privateMedia/), migrarrecordings/recordingUpload.ts,recordings/recordingJob.ts,content/contentPieceUpload.ts,content/contentPieceLink.ts,content/contentPieceJob.ts(espeech/speechMediaPipeline.tsse couber sem arrastar escopo). Prova: unit do guard (teto por chunk/Content-Length mentiroso) + int/e2e existentes verdes. - F2 — resíduos DRY do domínio
content. Selects da peça num módulo (contentPieceData.tsou o loader) reusados pela action/rotas; ponto único do reaper;curatedFieldssem generalidade morta. Prova: int de C211 verde sem mudança de asserção. - F3 — apertar os asserts.
tests/int/contentPiece.int.spec.tse a seção C211 do e2e: valores exatos demedia/filesize, removerreplace('Peça ', ''). Prova: os próprios specs.
Já resolvido no simplify (não reabrir)
revalidateTagdo listing na collection (afterChange→revalidateContentPiecesListing).- Passo
catalogandomorto no job; duração só quando medida; narrowing demediana resolução de fonte. - Peça
textoarquivando o arquivo;filenamepreservado no anexo; guard de "já tem arquivo" no util (não só na rota). CONTENT_PIECE_UPLOAD_SAFE_MESSAGEScompartilhado e mensagens de limite alcançáveis.SetCampaignPageChromena ficha + seta única; preview de texto; labels pt-BR decuratedFields;postCampaignJsonno diálogo de link;contentPieceStepLabelscomo dono único do passo; loader sem reaper (write fora do render); sem re-fetch do município em update parcial.
Explicitamente fora (descartes e defers com gatilho)
- VM carrega
statusLabel/processingLabel/retryHref/isPublishedsem consumidor de produção — descartar: é o padrão da casa (os VMs derecording/reelfazem igual; o VM é o contrato de apresentação do wire). deriveContentPieceCatalogIndexlê o município quando a relação é tocada (1 query por save de ficha) — descartar: micro-otimização sem N+1 real.- Bloco "Sem mídia oficial" da ficha para peça de arquivo — descartar: ramo defensivo correto para peça-link (a linha de arquivo só nasce com mídia).
- Pin de
contentPiece/contentMediaemcampaignFixtures— defer, gatilho: 2º spec que precise das fixtures (o int spec limpa as próprias linhas; precedente C199). normalizeForSearchnão dobra×→x— defer, gatilho: uso real da variante tipográfica na base ou próximo item que já toque o normalizador/backfill desearch_text(aí o backfill + trigram vêm de carona).- Picker de município (435
<option>no<select>nativo) — defer, gatilho: 2º consumidor do picker (filtros públicos do S27/S28) ou feedback real de UX na ficha.
Self-score (decision-quality)
- Decisões caras com rejeitadas? Sim — o lote é cheap_polish; nada de access/LGPD/schema/unicidade entra (piso 4–5 não se aplica), e os descartes/defers estão nomeados com gatilho.
- Cabe no appetite? Sim (~0,5–1 dia, três fases pequenas, sem migration).
- Rabbit holes nomeados? Sim — extrair helper com um call site (defer), refatorar o normalizador compartilhado (defer), picker com um consumidor (defer).
- Depth check? Sim — reusa o dono de mídia privada existente; não cria módulo de pureza.
- Outcome do C211 intocado? Sim — só estrutura interna e asserts.
Score: 5/5.