# Transmissões — Regras e Portabilidade

> Versão analisada: `v6`. Módulo **novo na v6** (nada a comparar com a v5).

## 1. Regras de negócio

| # | Regra | Implementação | Consequência observável |
|---|---|---|---|
| R1 | O módulo só é exibido se `modulos.status` for verdadeiro para o slug `transmissoes`. | `ModulosServiceProvider` compartilha `$modulos`; `v6/resources/views/portal/layouts/modelo1/layout/sidebar/transmissao.blade.php:1`. | Com o módulo desligado (padrão do seed), nenhum player aparece, mesmo que haja transmissão ativa. |
| R2 | No máximo **uma** transmissão ativa por vez. | `ContentController.php:678-680` (store), `:1000-1002` (update), `:1571-1573` (setStatus). | Salvar uma transmissão ativa desativa as anteriores da mesma categoria. |
| R3 | O widget exibe o **primeiro** conteúdo ativo encontrado, sem ordenação determinística. | `->where('status', true)->first()` nos 6 arquivos de tema. | Com mais de um ativo (possível, ver A4), a escolha depende do plano de execução do banco. |
| R4 | O campo `link` aceita URL do YouTube ou ID cru (segundo o placeholder do formulário). | `v6/resources/views/prefecture/contents/transmissoes/form.blade.php`. | Na prática o ID cru **não funciona** — ver A2. |
| R5 | O link é normalizado na gravação. | `normalizeVideoLink()` (`v6/app/Helpers/functions.php:672-690`) e `sanitizeLink()` (`ContentController.php:595-608`). | Links sem `http(s)://` recebem `https://`; links "sem ponto" viram caminho relativo. |
| R6 | A URL do YouTube é convertida para o formato `embed`. | Parser inline nos 6 Blades. | `watch?v=ID` → `https://www.youtube.com/embed/ID`. |
| R7 | Hosts não-YouTube são exibidos em `iframe` com a URL original. | Ramo `elseif (isset($url_[2]))` do parser. | Player genérico para streams de terceiros. |
| R8 | URL irreconhecível → mensagem "Transmissão não disponível". | Ramo `else` do parser. | O card continua sendo exibido, com o alerta vermelho. |
| R9 | Título é obrigatório, até 120 caracteres; link é obrigatório, até 200. | `ContentRequest` case `transmissoes`. | Formulário não salva sem título e link. |
| R10 | A telemetria mede acessos à tela do CMS, não audiências. | `menuClicks('transmissoes')`. | `menu_orders.clicks` do slug `transmissoes`. |

## 2. Acoplamentos

### 2.1 Dependências de outros módulos

| Depende de | Tipo | Onde |
|---|---|---|
| `modulos` (infraestrutura global) | Flag de exibição | `v6/app/Providers/ModulosServiceProvider.php` |
| `contents` / `category_contents` / `links` | Infraestrutura genérica de conteúdo | `v6/app/Http/Model/Prefecture/Content.php` |
| `banners` | **Convivência de layout** (não funcional) | `modelo3..modelo6/layout/sidebar/banners.blade.php` renderizam transmissão e banners no mesmo arquivo |
| `resources` / permissões Sentinel | Autorização | `v6/app/Library/Helper/appHelpers.php:22` |

### 2.2 O módulo **não** depende de

- `sessoes` — a transmissão não se vincula a uma sessão específica;
- `tv-camara` — apesar de o rótulo do CMS ser "Video (Youtube)", não há
  integração com a tabela `tv_camaras`;
- `vereadores` / `mandatos`;
- `atas-e-pontos-de-frequencia` / `pauta-do-dia` / `requerimentos`.

Consequência: **não há metadados de sessão** (número da sessão, data, Authors),
nem agendamento, nem finautomático de encerramento da transmissão.

## 3. Armadilhas

### A1 — Parser de URL duplicado em 6 arquivos de tema

A mesma lógica PHP (~35 linhas) está inline em:

| Arquivo |
|---|
| `v6/resources/views/portal/layouts/modelo1/layout/sidebar/transmissao.blade.php` |
| `v6/resources/views/portal/layouts/modelo2/layout/sidebar/transmissao.blade.php` |
| `v6/resources/views/portal/layouts/modelo3/layout/sidebar/banners.blade.php` |
| `v6/resources/views/portal/layouts/modelo4/layout/sidebar/banners.blade.php` |
| `v6/resources/views/portal/layouts/modelo5/home/news/sidebar/banners.blade.php` |
| `v6/resources/views/portal/layouts/modelo6/layout/sidebar/banners.blade.php` |

Consequências:

- **Impossível de testar em unidade** (lógica em apresentação, sem classe).
- Qualquer correção precisa ser aplicada 6 vezes — risco de divergência.
- O projeto **já tem** uma implementação reaproveitável e mais robusta:
  `embedVideo()` em `v6/app/Helpers/functions.php:607` (suporta YouTube, Vimeo,
  Facebook e outros) — o widget **não a utiliza**.

### A2 — O ID cru do YouTube, aceito no formulário, não funciona

Cadeia verificada:

1. O formulário promete: `Exemplo: https://www.youtube.com/watch?v=YbJOTdZBX1g ou YbJOTdZBX1g`.
2. `normalizeVideoLink()` adiciona `https://` → `https://YbJOTdZBX1g`.
3. `sanitizeLink()` (`ContentController.php:603-605`) detecta `https://` sem
   ponto e devolve `/YbJOTdZBX1g`.
4. No widget, `explode('/', '/YbJOTdZBX1g')` → `['', 'YbJOTdZBX1g']`;
   `isset($url_[2])` é **falso** → `$linkValido` permanece falso.
5. O cidadão vê **"Transmissão não disponível"**.

Ou seja: o ID cru é aceito na validação, gravado, e **rejeitado na exibição**.

### A3 — `linksUnique` é `hasOne` e pode ser nulo

`$transmissao->linksUnique->link` é acessado sem verificação. Um conteúdo da
categoria sem `Link` correspondente (por exemplo, importado por seed ou por
`update` que só altera o `title`) provoca **erro fatal** na renderização do
portal. O `update()` só cria o `Link` quando não existe nenhum
(`ContentController.php:1065-1080`), o que reduz — mas não elimina — o risco.

### A4 — A exclusividade de status está acoplada ao slug e é incompleta

- **No `store`** e no `update`**, a desativação usa `where('category_contents_id', ...)`,
  sem filtro de `order` → funciona.
- **No `setStatus`** (toggle por AJAX), a desativação inclui
  `->where('order', $content->order)`. Como o formulário de transmissões **não
  tem campo `order`**, `contents.order` é `NULL` para todos os registros
  (ver `ContentController.php:645`, `'order' => $request->get('order')`).
  Em PostgreSQL, `WHERE "order" = NULL` **nunca** é verdadeiro → **nenhuma
  linha é desativada**.

Resultado: **ativar pelo botão de toggle pode deixar duas transmissões ativas**,
violando R2. Como o widget não ordena (R3), a exibida passa a depender do plano
de execução do banco.

- O **`update`** desativa os ativos da categoria **incondicionalmente** (não
  verifica `$request->get('status') == 1`, ao contrário de `anuncios` e
  `super-banners`). Editar uma transmissão **desativada** apaga a transmissão
  que estava no ar.

### A5 — A view SQL não tem ordenação determinística para o portal

`row_number() OVER (ORDER BY c.created_at DESC)` numerifica a listagem do CMS,
mas **não** define a ordem física. E a view não é usada pelo portal: o widget
consulta `contents` via Eloquent, sem `orderBy`. Ver R3.

### A6 — Subselect escalar na view

`WHERE c.category_contents_id = (SELECT id FROM category_contents WHERE slug = 'transmissoes')`
quebra (`more than one row returned by a subquery used as an expression`) se o
slug estiver duplicado. O seed usa `updateOrCreate` por slug, mas a tabela não
tem `UNIQUE` sobre `slug`.

### A7 — Consultas Eloquent dentro da view

Cada renderização de página do portal dispara uma query extra
(`Content::whereHas('categoryContents', ...)->where('status', true)->first()`)
**por tema carregado**, dentro do Blade. Não há cache. Com o cache
`rememberForever('modulos')` do `ModulosServiceProvider`, o custo se repete a
cada request HTTP.

### A8 — Slug duplicado na view SQL e no slug da categoria

`transmissoes` é ao mesmo tempo: nome da view SQL, slug de
`category_contents`, slug de `modulos`, slug de `resources`, slug de
`menu_orders` e slug do `ContentController`. Qualquer colisão de nome cria
conflito de namespace entre tabela e view.

### A9 — `order` NULL e `update` de slug

O `update()` do conteúdo genérico sempre encontra o próprio registro na
consulta de unicidade de slug e, para categorias cuja raiz não é a categoria 1,
reescreve o slug do conteúdo para `pub{id}-{slug}`. Transmissões ficam com
slugs artificialmente prefixados — inofensivo hoje (o widget não usa `slug`),
mas é ruído de dados.

### A10 — Código morto no modelo1

`v6/resources/views/portal/layouts/modelo1/layout/sidebar/banners.blade.php:13-15`
consulta `$transmissao` e nunca a usa: uma query wasted por página no tema1.

## 4. Código duplicado entre v5 e v6

Não aplicável: **o módulo não existe na v5**. Não há rota, migration, model,
view, seed ou asset relacionado a transmissões em `v5/`.

## 5. Onde a v6 divergiu da v5

Não há linha de base: a funcionalidade é **introduzida** na v6. O que a v6
entrega que a v5 não entregava:

- Publicação de um vídeo de sessão pelo CMS.
- Exibição de um player ao vivo no portal.
- Controle de ativação/desativação com exclusividade (parcialmente — ver A4).

## 6. Regras que **não podem mudar** na reconstrução (v7)

1. O player continua na **barra lateral** de todas as páginas (é o único
   ponto de entrada visível ao cidadão).
2. Continua existindo **no máximo uma** transmissão ativa — e o operador
   continua com a troca simples de status em um clique.
3. O **texto** exibido ao público é "Transmissão", e o formato é 16:9.
4. O módulo continua **ligável/desligável** por município (`modulos.status`),
   com padrão desligado.
5. A entrada do link continua aceitando **URL do YouTube** e ID (mas o ID
   **precisa passar a funcionar de fato** — A2).
6. Quando o vídeo não está disponível, o cidadão vê
   **"Transmissão não disponível"** (não um card vazio).
