Sinalização Digital — Monitores + Anúncios
Uma tela em um local não é uma página web. Ela funciona sem supervisão por meses em um pendrive de TV barato, ninguém está olhando o console, e o modo de falha é um quadro congelado que ninguém percebe por uma semana. Cada decisão abaixo existe para sobreviver a isso.
Este skill fornece um módulo completo: CRUD de administração para mídia e playlists, um registro de displays, pareamento por PIN e URL de provisionamento, um player fullscreen, e a camada operacional (saúde, pré-visualização, controle remoto) que o torna gerenciável.
Quando usar
Construindo ou estendendo telas que exibem mídia em loop em um espaço físico, no Next.js App Router com Firestore ou Supabase/Postgres.
Quando NÃO usar
- Servidor de anúncios web (impressões, licitações, pixels de rastreamento, tags de terceiros) — um problema diferente com infraestrutura diferente.
- Quiosques interativos onde o usuário toca para transacionar. Sinalização é unidirecional.
- Um único vídeo incorporado em uma página de marketing. Use uma tag
<video>. - Configuração genérica de Next.js, CMS ou autenticação — assume-se que já existe.
Arquitetura
ADMIN (navegador) SERVIDOR DISPOSITIVO (TV)
───────────────── ────── ───────────
upload de mídia ─────────────► armazenamento de objetos ◄──── URL pública/assinada ─┐
CRUD de anúncios ────────┐ │
edição de playlist ──────┼──► /api/admin/* ──► anúncios + displays ──┐ │
emitir comando ──────────┘ (auth de staff) (escopo) │ │
▼ │
parear tela ◄──── PIN ───────────► /api/display/pair │ │
gera token por │ │
display ▼ │
/api/display/playlist ─── poll ───────────────────┤
▲ resolve adIds → anúncios │
└── carrega telemetria para cima, │
comandos para baixo loop do player
Um único tipo de requisição sustenta todo o runtime: o dispositivo faz poll em /api/display/playlist, enviando telemetria para cima e recebendo conteúdo e comandos para baixo. Tudo operacional viaja nesse canal.
Fatos críticos — leia antes de projetar qualquer coisa
- A playlist é um array ordenado no display, não uma entidade separada.
display.adIds: string[]é a playlist — a ordem é livre, sem joins, uma única leitura. Introduza uma entidade de playlist independente apenas quando o mesmo conteúdo precisar rodar em várias telas; veja data-model.md para a compensação e extensions.md para a migração. - Faça poll; não faça streaming. Um poll de 30–60 s é mais barato, sobrevive a pilhas de rede dormentes e reconecta-se sem custo. Tempo real é uma atualização opcional, não o padrão. Armazene em cache a resposta com um ETag ou você pagará por uma leitura completa por tela por poll.
- Uploads de mídia vão do navegador → armazenamento diretamente, nunca através de uma rota de API. Handlers de rota têm limites de tamanho de corpo e queimam computação ao fazer proxy de bytes.
- O dispositivo não é confiável e não é supervisionado. Ele mantém uma credencial de longa duração no
localStorageem hardware que qualquer um pode acessar. Essa credencial deve ser por tela e revogável. - O player nunca deve poder parar. Cada elemento de mídia recebe um timeout, um handler de erro e uma forma de pular. Um recurso quebrado avança; não trava.
Regras rígidas
Nunca derive o intervalo de poll do estado de renderização. Faça poll em um intervalo estável e leia o estado do slide a partir de uma ref. Um timer cujo efeito depende do índice do slide é destruído e recriado a cada slide, então uma atualização maior que um slide nunca dispararia; o contrato de comportamento fixa o tempo do poll no tempo real do relógio de parede.
Nunca sobrecarregue um único booleano como "pausado" e "deletado". Use
activepara intenção do operador e umdeletedAtseparado para ciclo de vida, ou um item deletado reaparecerá assim que alguém o religar.
Nunca confie em um escopo de inquilino/local fornecido pelo cliente. Derive-o no servidor a partir da sessão autenticada do staff ou da linha do display que o token resolve, e aplique-o em toda rota
[id]— retornando 404, não 403, para que os ids não possam ser sondados.
Nunca emita um token compartilhado para cada tela. Gere um token por display no pareamento, armazene apenas seu hash e faça da revogação uma única atualização de linha.
Nunca inicie o timer de duração de uma imagem antes de
onLoad. Em uma TV lenta, o slide expirará antes de ficar visível.
Nunca carregue estado de tela durável em um comando de uso único. Modo blank e takeover devem sobreviver à recarga diária do dispositivo e a um ciclo de energia — entregue-os como estado em cada poll (
mode), e mantenha o canal de comandos com confirmação de limpeza (ack) para comandos genuínos de uso único, como recarregar. E qualquer comando cujo efeito seja uma recarga deve persistir seu ack antes de recarregar, ou o servidor o reentregará para sempre.
Início rápido
- Preencha o contrato de integração para seu aplicativo e confirme a renomeação de domínio — adaptation.md.
- Modele as entidades e escolha entre array vs. tabela de junção — data-model.md.
- Crie tabelas/coleções, índices, regras de segurança e o bucket de mídia — firestore-backend.md ou supabase-backend.md.
- Construa os endpoints do dispositivo e de administração, incluindo pareamento e verificação de token — api-routes.md.
- Pareie uma tela — pairing.md — e então insira o loop do player — player-runtime.md.
- Construa o back-office: biblioteca de mídia, lista de displays, editor de playlist — admin-ui.md.
- Adicione monitoramento de saúde, pré-visualização e controle remoto antes de colocar em produção — operations.md.
- Verifique contra a tabela de contrato de comportamento em player-runtime.md — puxe o cabo de rede, delete o anúncio atual no meio do loop, emita uma recarga — e envie os testes da máquina do player como cobertura de regressão.
Diretório de referências
Carregue a referência correspondente às palavras-chave de gatilho. Para design do zero, leia data-model.md primeiro.
O código em api-routes.md e operations.md é escrito contra Firestore como backend canônico; supabase-backend.md define todas as substituições — a tabela de junção substituindo adIds, e equivalentes SQL das funções de serviço.
| Cenário | Palavras-chave de gatilho | Referência |
|---|---|---|
| Encaixar isso em um aplicativo existente | adaptar, renomear, integração, costura, inquilino, aplicativo hospedeiro | adaptation.md |
| Entidades, campos, formato da playlist | esquema, modelo, Anúncio, Display, playlist, tabela de junção, exclusão suave | data-model.md |
| Backend Firestore/Firebase | Firestore, firebase-admin, regras de segurança, índice composto, Firebase Storage | firestore-backend.md |
| Backend Supabase/Postgres | Supabase, Postgres, RLS, migração, bucket de armazenamento, Realtime | supabase-backend.md |
| Endpoints, pareamento, tokens, cache | Rota de API, pareamento, PIN, token, ETag, verificação de escopo, zod | api-routes.md |
| Pareando uma tela | pareamento, PIN, URL de provisionamento, hub, credencial, desvincular | pairing.md |
| O loop do player de TV | player, loop, crossfade, travamento de vídeo, rotação, retrato, wake lock, offline | player-runtime.md |
| Interface de back-office | admin, upload, biblioteca de mídia, editor de playlist, URL de provisionamento | admin-ui.md |
| Saúde, pré-visualização, controle remoto | batida cardíaca, última visualização, offline, pré-visualização, recarregar, blank, takeover de emergência, log de auditoria | operations.md |
| Agendamento, reuso, relatórios | dayparting, data de início, campanha, playlist compartilhada, prova de exibição, precache, multi-zona | extensions.md |
| Por que os templates diferem de uma portabilidade ingênua | proveniência, ledger, justificativa, mantido, adicionado | provenance.md |