# API dos dispositivos

Base: `https://seu-dominio/api/v1`

Autenticação por **Bearer token**, emitido no pareamento. Sem cookie de sessão,
portanto sem CSRF. Todas as respostas são `application/json` com `Cache-Control: no-store`.

Formato padrão:

```json
{ "ok": true,  ...dados }
{ "ok": false, "error": "mensagem" }
```

---

## POST /pair

Troca o código de 6 dígitos por um token permanente. **Não requer autenticação.**

```http
POST /api/v1/pair
Content-Type: application/json

{
  "code": "482910",
  "brand": "lg_webos",
  "model": "LG 55UN7300",
  "resolution": "1920x1080",
  "os_version": "webOS 5.0",
  "app_version": "1.0.0"
}
```

**200**

```json
{
  "ok": true,
  "token": "kJ8x...", 
  "device": { "id": 12, "name": "Recepção", "orientation": "landscape",
              "volume": 0, "timezone": "America/Sao_Paulo" },
  "heartbeat_sec": 60
}
```

**422** — código inválido, expirado, ou limite de TVs do plano atingido.
**429** — mais de 10 tentativas do mesmo IP em 15 minutos.

> `brand` é validado contra whitelist; valor desconhecido vira `browser`.

---

## GET /playlist

Programação em vigor, já resolvida pelo agendamento no fuso da TV.

```http
GET /api/v1/playlist
Authorization: Bearer kJ8x...
```

**200**

```json
{
  "ok": true,
  "version": "a3f9c1e8b2...",
  "playlist": { "id": 5, "name": "Promoções", "play_mode": "sequential", "transition": "fade" },
  "schedule_id": 3,
  "source": "schedule",
  "items": [
    { "id": 41, "media_id": 18, "type": "video", "title": "Oferta de verão",
      "url": "/m/18?d=12&e=1755302400&s=9fA2...", "duration": 30, "muted": true,
      "mime": "video/mp4" },
    { "id": 42, "media_id": 21, "type": "youtube", "title": "Institucional",
      "url": "https://youtube.com/watch?v=...", "yt_id": "dQw4w9WgXcQ",
      "yt_kind": "video", "duration": 120, "muted": true }
  ],
  "device": { "id": 12, "name": "Recepção", "orientation": "landscape",
              "volume": 0, "timezone": "America/Sao_Paulo" },
  "ttl": 21600
}
```

Sem conteúdo aplicável:

```json
{ "ok": true, "version": "empty", "playlist": null, "items": [],
  "message": "Nenhum conteúdo programado para este horário." }
```

**Notas de implementação**

- `version` só muda quando itens, ordem ou durações mudam — use-a para evitar rebaixar
- `url` de arquivos locais é assinada e expira em `ttl` segundos; rebaixe antes disso
- Itens fora da janela de validade da peça já vêm filtrados
- `source`: `schedule` (por agendamento) ou `default` (playlist padrão da TV)

**401** com `"action": "repair"` — token revogado. Limpe o armazenamento local e
volte à tela de pareamento.

---

## POST /heartbeat

Sinal de vida e canal de comandos remotos. Envie a cada `heartbeat_sec`.

```http
POST /api/v1/heartbeat
Authorization: Bearer kJ8x...
Content-Type: application/json

{
  "version": "a3f9c1e8b2...",
  "app_version": "1.0.0",
  "resolution": "1920x1080",
  "error": null,
  "acks": [ { "id": 88, "ok": true, "result": null } ]
}
```

**200**

```json
{
  "ok": true,
  "server_time": "2026-08-15T14:32:10-03:00",
  "commands": [ { "id": 89, "command": "reload", "payload": null } ],
  "content_stale": false,
  "heartbeat_sec": 60,
  "device": { "id": 12, "name": "Recepção", "orientation": "landscape", "volume": 0 }
}
```

**Comandos possíveis**

| Comando | Ação esperada no player |
|---|---|
| `reload` | Rebaixar a programação |
| `clear_cache` | Descartar cache local e rebaixar |
| `identify` | Exibir o nome da TV em tela cheia por ~8 s |
| `reboot` | Recarregar a aplicação |
| `screenshot` | Capturar a tela (opcional; responda `ok:false` se não suportar) |

Confirme a execução no `acks` do heartbeat seguinte. Comandos não confirmados
expiram em 1 hora.

`content_stale: true` indica que a versão mudou — chame `/playlist`.

---

## POST /logs

Comprovação de veiculação, em lote. Máximo de 500 entradas por requisição.

```http
POST /api/v1/logs
Authorization: Bearer kJ8x...
Content-Type: application/json

{
  "entries": [
    { "media_id": 18, "playlist_id": 5, "schedule_id": 3,
      "played_at": "2026-08-15 14:30:00", "duration_sec": 30, "outcome": "played" },
    { "media_id": 21, "playlist_id": 5, "schedule_id": 3,
      "played_at": "2026-08-15 14:30:30", "duration_sec": 0,  "outcome": "error" }
  ]
}
```

**200** — `{ "ok": true, "received": 2 }`

`outcome`: `played` · `skipped` · `error`.

Acumule localmente e envie em blocos — uma requisição por peça exibida derruba o
servidor com poucas dezenas de TVs. Em caso de falha, mantenha o lote na fila e
tente no ciclo seguinte; timestamps absurdos (relógio da TV errado) são
normalizados no servidor.

---

## POST /events

Erros e avisos operacionais. Máximo de 50 por requisição.

```json
{
  "events": [
    { "level": "error", "code": "media_load_failed",
      "message": "Vídeo 18 não carregou após 3 tentativas" }
  ]
}
```

`level`: `info` · `warning` · `error`. Aparecem em **Relatórios → Eventos das TVs**.

---

## Códigos de erro

| Código | Significado | O que o player deve fazer |
|---|---|---|
| 401 | Token ausente ou inválido | Se `action: "repair"`, limpar token e parear de novo |
| 422 | Dados inválidos | Corrigir e não repetir automaticamente |
| 429 | Rate limit | Aguardar; aplicar recuo exponencial |
| 500 | Erro no servidor | Continuar tocando do cache; tentar no próximo ciclo |

---

## Exemplo mínimo de cliente

```javascript
var token = localStorage.getItem('sgn_token');

function api(method, path, body, cb) {
  var xhr = new XMLHttpRequest();
  xhr.open(method, '/api/v1' + path, true);
  xhr.setRequestHeader('Content-Type', 'application/json');
  if (token) { xhr.setRequestHeader('Authorization', 'Bearer ' + token); }

  xhr.onload = function () {
    var data = null;
    try { data = JSON.parse(xhr.responseText); } catch (e) {}

    if (xhr.status === 401 && data && data.action === 'repair') {
      localStorage.removeItem('sgn_token');
      location.reload();
      return;
    }
    cb(xhr.status >= 200 && xhr.status < 300 ? null : new Error('http'), data);
  };

  xhr.send(body ? JSON.stringify(body) : null);
}
```

Implementação completa e testada em TV: [`public/assets/js/player.js`](../public/assets/js/player.js).
