# Impl: Biblioteca de reels na vertical Comunicação Status: aprovado Atualizado em: 2026-09-18 Issue: #1153 Intenção: docs/plans/reels-biblioteca.md Appetite restante: herdado (~1–2 dias eng; lista + detalhe + kill switch verificáveis com fixtures) ## Leitura da intenção - **Outcome:** `/campanha/comunicacao/reels` lista os reels **publicados** (capa 9:16 + título), o detalhe abre o reel em 9:16 e oferece os arquivos disponíveis (vídeo sem áudio primário, vídeo com áudio, mp3, `.srt`, transcrição) para a assessoria trabalhar fora do site; despublicar tira da lista sem apagar e republicar reativa; acesso só a `communicator`/`coordinator`/`candidate`, fail-closed. - **O que NÃO negociar:** arquivos privados (rota C193 é a única porta; `Media.read` intocado); nada de publicação/edição de vídeo na UI; nenhum reel some por acidente (kill switch reversível); sem busca/filtros no MVP (`-publishedAt`); sem `Consent` novo; sem migração (C193 fechou o modelo). - **O que reavaliar:** a recomendação A da intenção para o link direto do despublicado **conflita com o C193 (decisão B, mergeada)** — vale B: `draft`/`unpublished` não servem mídia; o detalhe interno continua abrindo, mas sem player/downloads. Verifiquei que o regex `/acervo/[^/]+` do chrome (`campaignPageChrome.ts:297`) **não** engole `/reels` (só falta a rule da lista). O conflito C184 (#1106) **não foi mergeado** (commit docs-only) — prosseguir. Os defers do C193 (`reelMediaPath`/`isReelStatus`) têm gatilho explícito neste item. ## Abordagem recomendada ```mermaid flowchart TD L["Lista /campanha/comunicacao/reels"] -->|"loadReelLibraryPageData · status=published · sort -publishedAt"| DB[("reel + reelMedia (depth 1)")] D["Detalhe /campanha/comunicacao/reels/:id"] -->|"loadReelDetailPageData · qualquer status"| DB D --> P["Player 9:16 + painel de downloads"] P -->|"video-audio ?? video · ?download=1"| M["GET .../media/kind — rota C193: só published"] D --> K["ReelPublicationPanel"] K -->|"POST .../publicacao"| A["setReelPublishedForActor: gate fresco + update único"] A --> DB ``` **D1 — Dados da página. Opções: A | B | C — Recomendação: A.** Loader em `src/utilities/reels/reelPageData.ts` (`server-only`, `overrideAccess: false, user`, select enxuto), view models puros no dono do vocabulário `src/lib/reel.ts` (como `lib/speechCut.ts` faz) e URL curta em `src/utilities/reels/reelListUrl.ts` (só `page`, reusando `campaignListUrl`). Rejeitadas: **B** query inline nos `page.tsx` (espalha o where do kill switch e mata o int sem HTTP); **C** top-level `src/utilities/reelPageData.ts` (top-level é pinado por `codebaseConventions.unit.spec.ts`; a subpasta `utilities/reels/` já é o dono criado no C193). **D2 — Render da lista. Opções: A | B | C — Recomendação: A.** Grid RSC de capas 9:16, todo o card é um `` (sem ilha cliente), com os shells de lista (`CampaignPageShell`, `CampaignListPendingBoundary/Results`, `CampaignListEmptyState`, `CampaignListFooter`). Rejeitadas: **B** `CampaignTable` (o sistema de listas é de tabela; o design é capa-led e o job é varrer por capa/título); **C** busca/filtro/sort toggle no MVP (produto recomenda A; ordem fixa `-publishedAt`). **D3 — Transporte do kill switch. Opções: A | B | C — Recomendação: A.** `POST .../reels/[id]/publicacao` com `campaignJsonMutationRoute` + `setReelPublishedForActor` em `src/app/(campaign)/campanha/actions/reels.ts` (gate fresco `canReadCommunicationCatalog`, find ator-escopado, **um** `payload.update`); painel cliente com `useState` pending + `postCampaignJson` + `router.refresh()` (precedente `SpeechCutPublicationPanel.tsx:42-65`, regra `campanha-action-feedback`). Rejeitadas: **B** server action + `