# `presentation` — kiosk/telão de dashboards e/ou decks

Fonte de verdade: `schemas/v2/presentation.schema.json` +
`api/cli/v2/mappers/presentation.ts` (horus-backend). Uma `presentation` é
uma playlist: uma sequência ordenada de **itens**, cada um apontando para um
`dashboard` inteiro ou para um `deck` inteiro, publicável como página pública
`/p/<sqid>`. Ela não tem elementos próprios — quem desenha conteúdo é o
`deck` (ver `pptist-json.md`) ou o dashboard já existente no app.

## Body

```yaml
nome: Kiosk Loja           # obrigatório, string não-vazia
descricao: Vitrine          # opcional, string ou null
dark_mode: true              # opcional, bool (default false no editor)
mode: manual                 # automatic | manual (default automatic)
base_width: 1920             # opcional, inteiro >= 1 (default 1920)
items:                       # obrigatório, array (ver "Item" abaixo)
  - ...
```

- **`dark_mode`** — tema escuro no player do kiosk.
- **`base_width`** — largura de referência em pixels da tela física alvo, usada
  para escalar o conteúdo. Presets comuns do editor: `1280` (HD, escala
  1.5x), `1920` (Full HD, escala 1.0x — default), `2560` (QHD, 0.75x),
  `3840` (4K/UHD, 0.5x). Qualquer inteiro positivo é aceito; use o mais
  próximo da resolução real da TV/monitor onde o kiosk vai rodar.
- **`mode`**:
  - `automatic` — avança sozinho de item em item, cada um ficando na tela por
    `duration` segundos (ver semântica de `duration` abaixo).
  - `manual` — não avança sozinho; precisa de navegação manual (clique,
    controle remoto). Bom para totens com atendente ou apresentações
    conduzidas ao vivo.
- Campos **read-only**, nunca no corpo editável — vêm só em `lumo info`/no
  resumo da listagem, geridos pelos verbos `publish`/`unpublish`:
  `publicado` (bool) e `url` (a própria URL pública). Não tente setá-los via
  `push`.

## Item

```yaml
items:
  - type: dashboard          # dashboard | deck — obrigatório
    dashboardId: 501          # obrigatório QUANDO type: dashboard
    order: 0                   # obrigatório, inteiro >= 0
    duration: 15                # obrigatório, inteiro >= 0 (segundos)
    title: Vendas                # opcional, string ou null — rótulo exibido no kiosk
    filters: null                 # opcional — passthrough JSON de filtros do item

  - type: deck
    deckId: 77                  # obrigatório QUANDO type: deck
    order: 1
    duration: 20
```

### A regra condicional que o `lint` aplica

`lint` valida a forma via `if/then` no schema — **não é só recomendação**:

- `type: dashboard` ⇒ `dashboardId` é **obrigatório e não-nulo** (um inteiro
  positivo real). Faltar a chave OU escrever `dashboardId: null` num item
  `type: dashboard` falha o lint do mesmo jeito.
- `type: deck` ⇒ `deckId` é **obrigatório e não-nulo**, mesma regra.

Não é preciso (nem incorreto) zerar explicitamente o campo do outro tipo — os
testes de fixture do CLI usam `dashboardId: null` num item `type: deck` só
por clareza documental; **omitir o campo que não se aplica também é válido**.

### Semântica de `duration` — cuidado, não é uniforme entre os dois tipos

`duration` é sempre em **segundos**, mas o que ele mede muda com `type`:

- **`type: dashboard`** — `duration` é o tempo **total** que aquele dashboard
  fica na tela antes de avançar pro próximo item.
- **`type: deck`** — `duration` é o tempo **por slide interno do deck**, não
  o tempo total do item. Um deck item com `duration: 20` e um deck de 4
  slides fica **80 segundos** na tela no total (20s por slide, avançando
  sozinho slide a slide antes de passar pro próximo item da presentation).

Em `mode: manual`, `duration` é ignorado (nada avança sozinho).

### `order`

Define a posição na sequência — não precisa ser contíguo nem começar em 0,
mas mantenha-o assim por clareza; o player ordena por esse campo, não pela
ordem de aparição no array.

## Kiosk misto (dashboard + deck na mesma sequência)

```yaml
id: 12
kind: presentation
lumo: v2
tenantId: 550
---
nome: Kiosk Loja
descricao: Vitrine
dark_mode: true
mode: automatic
base_width: 1920
items:
  - type: dashboard
    dashboardId: 501
    order: 0
    duration: 15
    title: Vendas
  - type: deck
    deckId: 77
    order: 1
    duration: 20
    title: Resumo do mês
```

Isso é o caso de uso central de `presentation`: alternar telas prontas do
dashboard (interativas, atualizadas em tempo real quando abertas fora do
kiosk) com decks narrativos desenhados à mão — mesma sequência, mesmo link
público.

## Publicar, checar, tirar do ar

```bash
lumo presentation publish presentation:12    # idempotente — imprime a URL /p/<sqid>
lumo presentation check presentation:12      # GET na URL pública; sai não-zero se não resolver
lumo presentation unpublish presentation:12  # tira a URL pública do ar
lumo presentation open presentation:12       # URL do EDITOR autenticado — não é a pública
```

- `publish`/`unpublish`/`check` não usam `If-Match` — nenhum deles toca campo
  de corpo relevante a push/pull (`nome`, `dark_mode`, `mode`, `base_width`,
  `items`), então não há o que dar conflito de versão. Publicar uma
  presentation já publicada apenas reconfirma a mesma URL.
- `check` **não republica**: ele lê o link atual (`GET /presentation/:id/url`,
  leitura pura) e faz um GET simples nele, reportando 2xx/erro. Assim uma
  identidade só-leitura (CI/agente) consegue rodar o gate, e o rastro de
  auditoria (`publicado_em`/`publicado_por`) não é reescrito a cada checagem.
  Rode depois de qualquer edição de `items` antes de entregar o link para
  alguém — é o único jeito de confirmar "de fora" que o kiosk está no ar, sem
  abrir um browser.
- `open` é a URL do **editor** (autenticado, mesma sessão do browser do
  usuário) — não confundir com a URL pública que `publish`/`check` usam.

## Deck no telão (sem montar presentation na mão)

Um `deck` também vai ao telão por si:

```bash
lumo deck publish deck:77                 # publica e imprime a URL pública /p/<sqid>
lumo deck publish deck:77 --seconds 10    # 10s POR SLIDE (não é a duração total do deck)
lumo deck url deck:77                     # leitura pura: URL + estado; sai 0 mesmo se não publicado
lumo deck check deck:77                   # GATE de CI: a URL pública resolve (2xx)?
lumo deck unpublish deck:77               # tira do ar
```

- **`secondsPerSlide` é por slide.** O deck tem N slides; cada um fica esse
  tempo na tela (default 5s, servidor limita a 1–3600). Não confunda com o
  `duration` de um item de presentation, que é a duração do item inteiro.
- **Nunca use `publish` só para descobrir a URL.** `url` e `check` são leitura
  pura (`GET /deck/:id/url`): não exigem permissão de publish nem reescrevem
  `publicado_em`/`publicado_por`. `url` imprime o estado e sai com sucesso
  mesmo se o deck não estiver publicado (é estado, não erro); `check` é o gate
  e sai não-zero se não estiver publicado ou se o link não resolver.
- **Modelo:** o kiosk continua sendo uma *apresentação*. Publicar um deck
  cria (ou reusa) a apresentação que contém só aquele deck e publica ela. Ela
  aparece no `lumo presentation list` — esperado, não é bug (no webapp ela fica
  escondida, porque lá é encanamento). Só monte a presentation à mão quando
  quiser de fato um kiosk com **vários** itens.
