Título da gravação
+dd/mm/aaaa
+O arquivo ainda está sendo enviado
++ Player e download aparecem quando o envio terminar. +
+` (contrato de `buildWatchHref`, `speechViewModels.ts:233-251`); excerpt renderizado com o `SpeechHighlightParts` existente; nenhum score exposto. + +## Componentes / mudanças + +- **`Recording`** (`src/collections/Recording.ts`, novo): slug `recording`; labels `Gravação`/`Gravações`; `admin.group: 'Comunicação'`; `useAsTitle: 'title'`; access `create/update = canUploadRecording`, `read = canReadRecording`, `delete = canDeleteRecording`; hook `stampCampaignCreatedBy`; campos `title` (maxLength), `recordedAt` (date, index, opcional — fallback de exibição no upload), `status` (select, index, default `uploading`), `step` (select, readOnly), `media` (upload → `recordingMedia`, readOnly), `durationSeconds`, `searchText` (textarea readOnly), `error` (textarea readOnly), `systemStampedActorField`. +- **`RecordingSegment`** (`src/collections/RecordingSegment.ts`, novo): slug `recordingSegment`; `admin.hidden: () => true`; access create/update/delete admin-only, read `canReadRecording`; campos `recording` (rel index), `order`, `startSeconds`, `endSeconds`, `text`, `searchText` com hook de derivação (espelho de `SpeechSegment.ts:13-18`). +- **`RecordingMedia`** (`src/collections/RecordingMedia.ts`, novo): slug `recordingMedia`; upload privado, `alt` obrigatório, access pelo D1/D2; nunca consumido por `/api/media/file`; `admin.group: 'Comunicação'` com descrição de privacidade (forma de `ReelMedia.ts:15-43`). +- **`src/utilities/access/recordings.ts`** (novo): `canReadRecording`, `canUploadRecording`, `canDeleteRecording` (predicados separados, nunca alias — forma de `access/speeches.ts:19-44`); reexport no barrel `campaignAccess.ts`. +- **`src/lib/recording.ts`** (novo): statuses/steps/labels pt-BR, `RECORDING_MAX_BYTES`, MIME aceito, títulos/mensagens de erro compartilhadas com o cliente (client-safe). +- **`src/lib/schemas/recording.ts`** (novo): zod do metadata do upload, retry, delete e status (padrão `lib/schemas/speechCut.ts`). +- **`src/lib/recordingTranscription.ts`** (novo, puro): `mergeChunkTranscriptions` (offset/clamp/dedupe), `recordingSearchText` (concatenação normalizada). +- **`src/lib/privateMedia.ts`** (novo; move `src/lib/reelMedia.ts:8-66`, nomes genéricos). +- **`src/utilities/privateMedia/privateMediaResponse.ts`** (novo; extraído de `src/utilities/reels/reelMediaResponse.ts:32-173`; a rota C193 passa a importar daqui). +- **`src/utilities/media/ffmpeg.ts`** (novo): `runFfmpeg`/`ffmpegBinary`/`messageOf` movidos de `speech/speechMediaPipeline.ts:17-29,73-98`, com `timeoutMs` opcional; `speechMediaPipeline.ts` mantém a API para os jobs da Câmara. +- **`src/utilities/ai/deepInfraTranscribe.ts`** (editar): adicionar `deepInfraTranscribeSegments` (verbose_json + segment, timeout 600 s) e normalização no dono. +- **`src/utilities/recordings/`** (novo): `recordingListUrl.ts` (state `{source,q,page}` sobre `campaignListUrl.resolveListUrl`), `recordingListFilters.ts` (where do D7), `recordingViewModels.ts` (labels, excerpt, hrefs), `recordingPageData.ts` (lista+detalhe), `recordingUpload.ts` (D3), `recordingJob.ts` + `recordingScheduler.ts` + `recordingTranscriptionJob` (D4/D5, puros extraídos). +- **Rotas** (`src/app/(campaign)/campanha/(app)/comunicacao/acervo/`): `gravacoes/enviar/route.ts` (POST cru, allowlist), `gravacoes/[id]/arquivo/route.ts` (GET Range), `gravacoes/[id]/retry/route.ts` (wrapper), `gravacoes/[id]/apagar/route.ts` (DELETE + `isSameOriginRequest`), `gravacoes/status/route.ts` (wrapper), `gravacoes/page.tsx` (redirect alias), `gravacoes/[id]/page.tsx` (detalhe); `page.tsx` do acervo ganha o branch do `source`. +- **Actions** (`src/app/(campaign)/campanha/actions/recording.ts`, novo): `retryRecordingForActor`, `deleteRecordingForActor` (transação segmentos+row; delete do `recordingMedia` pós-commit via `onPayloadTransactionCommit`, `payloadTransaction.ts:37-48`), `getRecordingStatusesForActor` (reaper preguiçoso entre os ids). +- **`payload.config.ts`** (editar): registrar as três collections e `recordingMedia: true` no `s3Storage` (`payload.config.ts:166-172`) — sem isso, prod cai no disco efêmero. +- **Manifest e2e** (`scripts/lib/e2e-affected-manifest.mjs`): prefixos novos (`src/utilities/recordings`, `src/utilities/privateMedia`, `src/utilities/media`, `src/components/campaign/recording`, `src/lib/recording`) na entrada da vertical (linhas 305-317); na entrada de risco `src/utilities/ai` (linhas 49-55, 261-264) somar `campaignSpeechAcervo` a `campaignAiTranscribe`. +- **Conventions** (`tests/unit/codebaseConventions.unit.spec.ts:202-225`): allowlist do POST cru com a razão (streaming de vídeo, cookie + same-origin explícito). +- **Migration:** ver §Migration abaixo. +- **Access / Consent:** `utilities/access/recordings.ts` (sem `Consent` — mídia interna de staff, sem PII; nenhum opt-in nasce aqui). +- **UI:** Impeccable **C** — port do artefato aprovado + os complementos do designer listados em §UI; shells reusados (`CampaignListFooter`, `CampaignListEmptyState`, `CampaignListResults/PendingBoundary`, `CampaignSearchInput`, `AlertDialog`, `Badge`, `Spinner`). Shape→craft→critique→polish no ciclo de UI. + +### Dados → forma (se aplicável) + +- **N/A por decisão da intenção** (`acervo-gravacoes-enviadas.md:54-56`): resultados de busca não são métrica nem agregado; nenhum score de relevância é exposto como número. Nada a apresentar; nenhuma forma nova. + +## UI e veredito do designer + +**Coberto pelo artefato (`docs/plans/acervo-gravacoes-enviadas-ui-design.html`) — port mecânico:** alternador de fonte (Cena 1/Cena 6); lista com card de gravação, estado, data·duração, excerpt e "Abrir" (Cena 1); os quatro estados em card — Enviando (inclui barra de progresso), Processando, Falhou com "Reprocessar transcrição", Pronto (Cena 2); detalhe Pronto com player, transcrição clicável, "Baixar" e chrome "Gravação enviada / Acervo" (Cena 3); diálogo de envio com arquivo/título/data (Cena 4); vazio (Cena 5); mobile lista+detalhe (Cena 6). Semântica que o implementador decide sem mudar estrutura: o `role="tablist"` do artefato vira dois ``s com `aria-current` e o mesmo estilo de pill (navegação, não tab widget); `` nativo no lugar do campo mascarado. + +**Falta superfície visual nova — o `designer` precisa estender ANTES do markup:** + +1. **Confirmação de apagar** (exigida pela intenção `:66`, ausente do artefato): `AlertDialog` com título/descrição/CTA destrutivo; definir se o gatilho vive só no detalhe ou também nos cards da lista; a copy deve dizer que remove transcrição e arquivo, irreversível (base: `SpeechCutDeleteDialog.tsx`). +2. **Detalhe em estados não-prontos**: `uploading` (sem arquivo), `processing` (player já disponível? aviso de transcrição pendente), `failed` (copy de erro + "Reprocessar transcrição") — o artefato só desenha o detalhe Pronto. +3. **Reprocessar em andamento**: feedback do clique (botão desabilitado/spinner ou a tela trocar para Processando) e onde o erro do retry aparece. +4. **Erro acima do limite** no diálogo: posição/copy do erro inline e o aviso do limite antes do envio (o artefato promete "O limite será informado antes do envio" mas não o renderiza; o valor é "Vídeos até 4 GB"). +5. **Progresso/estado do diálogo durante o envio**: a Cena 4 mostra só o formulário preenchido; se a barra da Cena 2 for portada para o diálogo, registrar como composição aprovada; senão desenhar. +6. **Paginação e vazio-da-busca da fonte nova**: reusar `CampaignListFooter` (shell existente) e definir a copy de "nenhuma gravação encontrada para X" (diferente do vazio Cena 5, que é "nenhuma gravação enviada ainda"). + +**Não é trigger (não inventa estrutura):** fiação de dados/copy no markup aprovado, reuso dos shells, destaque de trecho com `SpeechHighlightParts` (consistência com a lista da Câmara), rótulos de status do `lib/recording.ts`. + +## Migration + +- Nome sugerido: `pnpm migrate:create add_recording` → revisar o SQL gerado (`recording`, `recording_segment`, `recording_media`, enums `enum_recording_status`/`enum_recording_step`, FKs para `recording_media`/`campaign_user`, colunas em `payload_locked_documents_rels`, índices) e **editar à mão** os dois índices trigram, no padrão idempotente de `src/migrations/20260913_001200_add_speech_segment_trgm_index.ts:9-14`: `recording_search_text_trgm_idx` e `recording_segment_search_text_trgm_idx` (`pg_trgm` já instalado por migração anterior). Forma de referência: `20260918_201009_add_reel.ts`. +- **HARD-STOP de aprovação humana explícita, mesmo em sessão `--auto`, e local-only** (worktree, `pnpm migrate` no banco local; nunca tocar banco remoto/prod e nunca editar migração antiga). **Aprovada na sessão `--auto` (2026-09-18)** para criar/aplicar `add_recording` só no banco local do worktree. + +## Invariantes + +- Toda Local API em contexto de usuário com `overrideAccess: false` + `user` (leitura de lista/detalhe, media, retry, delete); bypass admin só onde o pipeline é dono do row/arquivo, com comentário justificando (forma de `speechCutJob.ts:42-83`). +- Escrita multi-collection em transação com `req` (`withPayloadTransaction`): finalizar upload (media+row), salvar transcrição (segmentos+row) e apagar (segmentos+row); o delete físico do `recordingMedia` roda pós-commit (`onPayloadTransactionCommit`) e é best-effort. +- Sem `Consent` novo (mídia interna de staff, sem PII); `Media.read` intocado; sem URL pública; `recordingMedia` fora do proxy `/api/media/file`. +- `leader`/`advisor` fail-closed em página (`gate: 'speechCatalog'`), access das collections e rota `/arquivo` (404 silencioso). +- Sem segundo cadastro de pessoa; transcrição somente leitura; nenhum item de nav novo; nenhum módulo top-level novo em `src/utilities/` (subpastas `recordings/`, `privateMedia/`, `media/`); pin de conventions/manifest atendidos. +- Identificadores em inglês; copy/labels em pt-BR; valores de URL em pt-BR onde o repo já usa (`source=enviadas`, `apagar`, `arquivo`, `enviar`; `retry` como no corte). +- "Edit the owner, don't twin": serving generalizado no módulo, `runFfmpeg` extraído, `pickMatchingSegment` movido ao lib; `Speech`/`SpeechSegment`/`Media`/`ReelMedia` intocados. +- `pnpm gate:fast` durante; `pnpm push` como entrega; e2e da superfície antes do push. + +## Fases verificáveis + +1. **Puro + schema + migration** (~1 dia). + - `src/lib/recording.ts`, `recordingTranscription.ts`, `privateMedia.ts`, `schemas/recording.ts`; collections + access + barrel; `payload.config.ts`; `pnpm generate:types`. + - Unit: statuses/limites/labels; merge de chunks (offset, clamp, dedupe, searchText); URL/parse/canonical do `source`; predicates (forma); headers puros movidos. + - Int: matriz de access das 3 collections (communicator/coordinator/candidate passam; advisor/leader/anônimo negados) e cascade de segmentos. + - **Migration `add_recording` — HARD-STOP humano, local-only.** +2. **Serving + upload + job** (~1,5 dia). + - `privateMedia/*` + troca da rota C193 (testes C193 verdes); rota `/arquivo`; upload (`enviar`) + allowlist; `deepInfraTranscribeSegments`; `media/ffmpeg.ts`; job + scheduler + reaper; retry/delete/status + actions. + - Unit: validação do metadata/limite; parser de Range (movidos); job puro. + - Int: upload (streaming para disco de teste, falha de tamanho/mime sem row fantasma, transição), job com `FFMPEG_PATH` fake + transcritor injetado (happy path multi-chunk com offsets; falha do provider → `failed` e arquivo preservado; retry reseta; reaper), `/arquivo` (206/416/404 e papéis), delete (segmentos+row+media), retry/status. +3. **UI** (~1 dia, após os complementos do designer). + - Toggle, lista+busca+footer+vazio, cards de estado, diálogo de envio (client, XHR + progresso), detalhe (player + transcrição clicável), refresher de status, reprocessar, apagar. + - Unit: view models (labels, excerpt, `watchHref` com `t`), toggle/aria, validação client-side de tamanho. +4. **E2E + gates** (~0,5 dia). + - Estender `tests/e2e/campaignSpeechAcervo.e2e.spec.ts` (herda o acervo existente): fixture de `recording` `ready` + segmentos + `recordingMedia` pequeno; toggle/`?source=enviadas`; busca com excerpt e `?t=`; detalhe com player/transcrição/Baixar; `/arquivo` 200 para comunicador e 404 para advisor/anônimo; contrato HTTP do upload (cross-origin/limite). Upload+ASR reais ficam no int (declarado). + - Manifest/conventions; `pnpm gate:fast`; `pnpm test:e2e:affected`; `pnpm push`. + +Quota: ~3,5–4 dias eng, dentro do appetite herdado; se apertar, o corte é a fase 3 (UI) para o essencial do artefato antes do polish. + +## Rabbit holes / Não escopo (engenharia) + +- Fila/worker/pipeline genérico, tempo real, progresso percentual por chunk (intenção cortou). +- Diarização/rotulagem por pessoa (C200); `Contact` nunca entra. +- Transcode, thumb real, normalização, legendas, mux (o placeholder do artefato serve). +- Upload resumível/chunked no browser, retomada de sessão, URLs assinadas S3, `clientUploads`. +- Merge das fontes, entrada no home-search, busca semântica (C192), cortes (C167/C174). +- Edição de transcrição, dossiês, publicação externa, comissões/outras fontes. +- Renomear `ReelMedia`/`Media`, rota pública nova, `Consent` novo, item de nav novo. +- Limpeza de mídia órfã fora do delete da própria gravação. + +## Riscos e mitigação + +- **Job morto por deploy/restart no meio de horas de transcrição.** Reaper de 60 min + heartbeat por chunk + retry que reprocessa do zero; estados honestos e arquivo preservado. +- **Disco efêmero do container com arquivo de vários GB.** Limite 4 GiB fail-closed (cliente e servidor), limpeza em `finally`, ENOSPC vira `failed` com mensagem mapeada; ops deve garantir headroom do container (nota de deploy). +- **Custo/limite do Deep Infra.** Chunks de 20 min (~4,8 MB, 32 kbps), timeout de 600 s por chunk, concatenação com offsets; falha isolada não publica transcrição parcial (uma transação só no fim). +- **LIKE casando na fronteira entre segmentos.** Fallback de excerpt por termos (`pickMatchingSegment` movido); nenhum resultado sem excerpt quando o `searchText` casou. +- **Chrome genérico engolindo `gravacoes` (lição C194).** Regra específica antes da genérica (`campaignPageChrome.ts:297`) + pin unit; alias `/acervo/gravacoes` redireciona. +- **Regressão do contrato Câmara.** `speechListUrl`/`speechListFilters`/`Speech` intocados; unit existente de URL/omnibox continua sendo o pino; o branch novo tem param set próprio. +- **Vazamento de mídia.** Access por papel em todas as camadas, rota sob `/campanha` com 404 silencioso, sem proxy público; matriz de papéis no int/e2e. +- **S3 não configurado em produção (`recordingMedia: true` esquecido).** Entra no aceite e no checklist do §Migration/config; `resolveS3StorageEnv` fail-closed no boot parcial. +- **e2e não cobre upload/ASR reais.** Decisão registrada: int cobre streaming e job (ffmpeg fake + transcritor injetado); e2e cobre a superfície HTTP/UI com fixture pronta. + +## Débitos do /simplify (triage autônoma) + +- **Registrado:** gates de upload/retry/delete por `canReadCommunicationCatalog` em vez dos predicados de escrita do dono → **C202** (#1186), `depends: [C199]`, plano `docs/plans/gates-gravacoes-predicados.md` (score 4, expensive_lock; destrava com `pnpm agent:ready` após este merge). +- **Defer (gatilho):** `withSeek` reimplementa o `?t=&q=` de `buildWatchHref` — extrair só quando houver 3º consumidor do deep-link (C200/corte de gravação) ou edição conjunta dos dois view models. +- **Defer (gatilho):** busca da lista com 1 query de segmento por resultado — revisitar se o p95 da busca passar de ~1,5 s ou o catálogo de gravações passar de ~500 (trocar por query única/lateral ou excerpt pré-computado). +- **Defer (gatilho):** job cria `recordingSegment` um a um — revisitar quando um save de gravação de horas passar de ~1–2 min (bulk insert). +- **Defer (gatilho):** reaper só roda em consulta de status/lista/detalhe — revisitar se existir varredura periódica de mídia/auditoria de bucket (D5 manteve sem fila). +- **Defer (gatilho):** headroom de disco/ENOSPC no container com vídeos de horas — conferir no runbook antes do primeiro upload real de horas (risco já mitigado por cap de 4 GiB + cleanup). +- **Descartado:** `privateMediaResponse.ts` fundir storage+serving+download — consolidação deliberada de um dono único para o mesmo I/O privado; reabrir só com 2º consumidor real de storage. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção ainda coberto: enviar e acompanhar até Pronto; fonte nova pesquisável sem facetas; fala de qualquer pessoa com trecho+timestamps; detalhe com player/transcrição clicável/Baixar/Reprocessar em Falhou; estados visíveis; falha preserva o arquivo; só os três papéis; sem URL pública. +- [ ] Invariantes AGENTS/engineering-standards: `overrideAccess: false` + `user`; transação multi-collection; sem `Consent`; `Media.read` intocado; leader/advisor fail-closed; sem nav novo; sem top-level novo em `utilities/`; manifest e allowlist de conventions atualizados; migração nova sem editar antigas; `pnpm push`. +- [ ] Testes de domínio previstos (unit/int) onde access/write paths mudam: merges/offset e searchText; access das 3 collections + rota; upload/limite; job (feliz/falha/retry/reaper); serving com Range; delete em cascata. + +## Self-score de decision-quality + +**4,5/5.** As sete decisões caras têm alternativas honestas e rejeições ancoradas em file:line (contrato pinado da Câmara, `reelMedia` de domínio, endpoint interno do C193, ausência de parser no `package.json`, precedente do job/reaper). A abordagem cabe no appetite (schema→serving/job→UI→e2e, tracer cedo no upload+schema) e reusa shells/helpers/mecanismos existentes. Perde 0,5 porque duas escolhas ficam condicionadas a confirmação humana fora do código: a migração (HARD-STOP declarado) e os complementos visuais do designer (apagar, estados do detalhe, erro de limite, paginação) — sem eles o markup não fecha e o plano não pode fingir que o artefato cobre tudo. diff --git a/docs/plans/acervo-gravacoes-enviadas-ui-design.html b/docs/plans/acervo-gravacoes-enviadas-ui-design.html index 1ed71e17..195ff67a 100644 --- a/docs/plans/acervo-gravacoes-enviadas-ui-design.html +++ b/docs/plans/acervo-gravacoes-enviadas-ui-design.html @@ -29,9 +29,14 @@ font-family: Inter, ui-sans-serif, system-ui, sans-serif; } button, - input { + input, + a { font: inherit; } + a { + color: inherit; + text-decoration: none; + } .icon { width: 1rem; height: 1rem; @@ -70,6 +75,16 @@ color: #292524; border: 1px solid transparent; } + .btn-destructive { + background: #b42318; + color: white; + border: 1px solid #b42318; + } + .btn:disabled, + .btn[aria-disabled='true'] { + cursor: not-allowed; + opacity: 0.55; + } .field { min-height: 2.75rem; width: 100%; @@ -157,6 +172,11 @@ .seg-active { background: #f4f4f5; } + .alert-error { + border: 1px solid rgb(180 35 24 / 28%); + background: #fff7f6; + color: #8f1d14; + } @media (prefers-reduced-motion: reduce) { *, *::before, @@ -213,6 +233,20 @@+ + ++ + + ++ + + ++ + + @@ -280,18 +314,18 @@ - - +Selecionar arquivo localO limite será informado antes do envio.Vídeos até 4 GB · MP4, MOV, MKV e formatos compatíveis @@ -617,8 +651,8 @@ @@ -638,9 +672,13 @@-+ Cena 6 · Mobile (390 px) — lista e detalhe com transcrição
@@ -680,12 +718,15 @@- +- - + ++ + + ++ Cena 7 · Desktop — confirmação destrutiva ao apagar +
++++ ++ +++++ +Título da gravação
+dd/mm/aaaa · —:—:—
++ ++ + + ++++Apagar esta gravação?
++ O arquivo e toda a transcrição serão removidos. Esta ação não pode ser desfeita. +
++ + +++ Decisão visual: o gatilho “Apagar” vive somente no detalhe, no bloco de + ações do cabeçalho. Os cards da lista preservam uma única ação, “Abrir”, evitando uma ação + irreversível no contexto de varredura. +++ + + ++ Cena 8 · Desktop — detalhe em Enviando, Processando e Falhou +
++++++ + ++++ Enviando ++ +Título da gravação
+dd/mm/aaaa
++ ++O arquivo ainda está sendo enviado
++ Player e download aparecem quando o envio terminar. +
++ +++ Enviando arquivo…58% +++ + ++++ Processando ++ +Título da gravação
+dd/mm/aaaa · —:—:—
++ ++ ++++ + Transcrição em processamento +++ O vídeo já pode ser assistido. A transcrição ainda não está disponível. +
++ ++++ Falhou ++ +Título da gravação
+dd/mm/aaaa · —:—:—
++ ++++A transcrição não foi concluída
+Não foi possível processar o áudio. O arquivo foi preservado.
++ + +++ Em “Enviando”, não há arquivo utilizável. Em “Processando” e “Falhou”, o envio já + terminou: player e download ficam disponíveis; somente a transcrição é substituída pelo + aviso de estado. +
++ + + ++ Cena 9 · Desktop — reprocessamento em andamento e falha ao iniciar +
++++++ Processando + +Reprocessando a transcrição
++ O arquivo foi preservado. Você pode assistir enquanto a nova transcrição é + preparada. +
+ ++ Ao aceitar o pedido, a tela troca de “Falhou” para “Processando”. +
++ Falhou + +A transcrição continua pendente
++ O erro original continua visível até uma nova tentativa ser iniciada. +
+ ++ + Não foi possível iniciar o reprocessamento. Tente novamente. +++ O erro do clique aparece imediatamente abaixo da ação, sem apagar a mensagem da falha de + transcrição. Não usar toast como único feedback. +
++ + + ++ Cena 10 · Desktop — arquivo acima do limite no diálogo +
++ +++ +++++ +Enviar gravação
++ Adicione um arquivo local ao acervo. +
++ + ++ ++ + Este arquivo tem 4,6 GB. Escolha um vídeo de até 4 GB. +++ Vídeos até 4 GB · MP4, MOV, MKV e formatos compatíveis +
++ + +++ O limite aparece antes da seleção, dentro da área do arquivo. Quando excedido, o erro fica + logo abaixo do seletor, associa-se ao campo e bloqueia “Enviar”. +
++ + + +Cena 11 · Desktop — diálogo durante o envio
++ +++ +++++ ++ Enviando gravação +
++ Mantenha esta janela aberta até concluir. +
++++ + + ++++ 58% +gravacao-plenaria.mkv
++ Título da gravação · dd/mm/aaaa +
++ +++ Enviando arquivo…58% +++ Quando o envio terminar, a gravação aparecerá na lista como “Processando”. +++ +++ + + +Cena 12 · Desktop — paginação e vazio da busca
++++++ ++ Rodapé com resultados +
++ + + ++++— gravações encontradas
+ ++ ++ Busca sem resultado +
++ + + +++ Nenhuma gravação encontrada para “termo buscado” +
++ Tente outro termo ou limpe a busca para ver todas as gravações enviadas. +
+ ++ Este vazio substitui somente os resultados filtrados e não oferece novo envio. O vazio sem + gravações continua sendo a Cena 5, com a ação “Enviar gravação”. +
++ + + ++ Cena 13 · Mobile (390 px) — extensão dos estados críticos +
+++++ +Detalhe com falha
++++ + +++Gravação enviada
+Acervo
+++++++ Falhou +Título da gravação
+dd/mm/aaaa · —:—:—
++ ++++ + + +A transcrição não foi concluída
+Não foi possível processar o áudio. O arquivo foi preservado.
+++ +Confirmação de apagar
++ +++ + + + ++ Apagar esta gravação? +
++ O arquivo e toda a transcrição serão removidos. Esta ação não pode ser desfeita. +
++ + ++++ +Envio em andamento
++ +++ +Enviando gravação
++ Mantenha esta janela aberta até concluir. +
++++ ++gravacao-plenaria.mkv
+ 58% ++ ++Enviando arquivo…
++ Depois do envio, a gravação aparecerá como “Processando”. ++ +++Busca sem resultado
++++ +Acervo
++ +++ + + +++ Nenhuma gravação encontrada para “termo buscado” +
++ Tente outro termo ou limpe a busca. +
+ ++ +Anotações semânticas obrigatórias para o port
++++
+- + O alternador de fonte é navegação: dois links com +
+aria-current="page"no + ativo, mantendo a aparência de pill. Não é widget de abas. +- + “Data da gravação” usa o controle nativo +
+type="date". +- Todo texto visível, rótulo, feedback e mensagem de erro permanece em pt-BR.
++
+- + Toda ação destrutiva exige confirmação. “Apagar” aparece somente no detalhe e abre a + Cena 7; nunca apaga diretamente no card. +
+- + Um CTA domina por estado: “Baixar” em Pronto; “Reprocessar transcrição” em Falhou; + “Enviar” no formulário válido; “Apagar gravação” apenas dentro da confirmação. +
+- + Erros de campo ficam junto ao campo; erro de retry fica sob a ação; falha de + transcrição ocupa o painel do estado. Toast nunca é a única evidência. +
+