CHECKMATE API v1

Documentazione completa delle API pubbliche per il software player sui miniPC Linux. Tutte le chiamate usano JSON. Base URL: https://{host}/api/v1

1. Panoramica e flusso operativo

Il software player sul miniPC segue questo ciclo di vita, dall'accensione al funzionamento continuo:

PRIMA ACCENSIONE
  1. Provisioning  ─── ottieni device_id dal server
                       (QR code / claim manuale)
  2. Pairing       ─── reclama il device con hardware ID (MAC)
                       il device diventa non disponibile per altri

CICLO CONTINUO (dopo pairing)
  3. Manifest Poll ─── ogni 30s, scarica la programmazione completa
  4. Content Sync  ─── scarica/aggiorna i file media
  5. Sync Ack      ─── conferma download di ogni file
  6. Heartbeat     ─── ogni 30s, segnala stato al server
  7. Command Poll  ─── ogni 10s, esegui comandi remoti
  8. Render        ─── valuta schedule rules, mostra contenuti

Ogni chiamata API restituisce JSON. In caso di successo il body contiene i dati richiesti. In caso di errore il body contiene { "error": "messaggio" } con HTTP status appropriato.

2. Provisioning

Il provisioning registra un nuovo device nel sistema. Avviene una sola volta alla prima accensione. Ci sono due modalità: claim da QR code (gestito dal backoffice) oppure creazione diretta.

2.1 Crea sessione di provisioning

POST/provisioning

Crea una nuova sessione di provisioning. Genera un codice univoco (32 byte hex) valido 15 minuti. Questa chiamata viene fatta tipicamente dal backoffice, non dal device.

Nessun body richiesto.

Risposta — 201 Created

{
  "id": 1,
  "provisioning_code": "a1b2c3d4e5f6...",
  "status": "open",
  "device_id": null,
  "expires_at": "2026-03-01T12:15:00.000Z",
  "created_at": "2026-03-01T12:00:00.000Z"
}

2.2 Controlla stato sessione

GET/provisioning?code=xxx

Il device può fare polling su questo endpoint per sapere quando la sessione è stata claimed. Utile se il provisioning avviene da cellulare (scansione QR) e il device aspetta.

ParametroTipoDescrizione
codestring (query)Il provisioning_code da verificare

Risposta — 200 OK (sessione trovata)

{
  "id": 1,
  "provisioning_code": "a1b2c3d4e5f6...",
  "status": "open" | "claimed" | "expired",
  "device_id": null | 42,
  "expires_at": "...",
  "claimed_at": null | "..."
}

Errori possibili

StatusCausa
400Parametro code mancante
404Sessione non trovata

2.3 Claim (registrazione device)

POST/provisioning/claim

Il device chiama questo endpoint per "reclamare" una sessione di provisioning aperta. Il server crea il device, i monitor, e restituisce device_id e device_token. Il device deve salvare entrambi in modo persistente.

Body richiesto

{
  "provisioning_code": "a1b2c3d4e5f6...",   // obbligatorio
  "device_name": "Ristorante Roma 1",        // obbligatorio
  "device_ip": "192.168.1.100",              // opzionale
  "monitor_count": 2,                        // opzionale (default: 1, max: 3)
  "monitor_names": ["Sala", "Bancone"]       // opzionale
}

Risposta — 200 OK

{
  "device_id": 42,
  "device_token": "f8c3a2e1..."    // 64 char hex — salvare in modo sicuro!
}

Errori possibili

StatusCausaAzione device
400provisioning_code o device_name mancanteFix parametri e riprova
404Codice non valido o sessione non openRichiedere nuovo codice
410Sessione scaduta (> 15 minuti)Richiedere nuovo codice
500Errore server nella creazione deviceRiprova dopo 10s

Nota importante

Dopo il claim, il device_token non viene mai più restituito dal server. Se perso, bisogna creare un nuovo device. Il server salva solo l'hash SHA-256 del token.

3. Device Pairing (attivazione hardware)

Dopo il provisioning, il device deve "reclamare" il proprio record inviando un identificativo hardware univoco (MAC address della scheda di rete primaria). Una volta reclamato, il device non è più disponibile per altre attivazioni.

3.1 Reclama device (pairing)

PATCH/devices/{deviceId}

Il device invia il proprio MAC address come device_pairing_code. Il server lo salva e da quel momento il device risulta "attivato" nel backoffice.

Body richiesto

{
  "device_pairing_code": "A4:83:E7:2F:1B:9C"    // MAC address del miniPC
}

Risposta — 200 OK

{
  "id": 42,
  "device_name": "Ristorante Roma 1",
  "device_pairing_code": "A4:83:E7:2F:1B:9C",
  ...
}

Errori possibili

StatusCausa
400Nessun campo valido nel body
500Errore database

Come ottenere il MAC address

# Linux — MAC della prima interfaccia di rete fisica
cat /sys/class/net/$(ip route show default | awk '/default/ {print $5}')/address
# Output: a4:83:e7:2f:1b:9c

3.2 Verificare se un device è già attivato

Prima di tentare il pairing, il software deve verificare che il device non sia già reclamato da un altro miniPC. Un device con device_pairing_code != null è già attivato e non può essere reclamato.

GET/devices/{deviceId}

Controllare il campo device_pairing_code nella risposta. Se è null, il device è disponibile. Se contiene un valore, è già attivato su un altro miniPC.

Un operatore può scollegare un device dal miniPC fisico tramite il backoffice. Questo resetta il device_pairing_code a null, rendendo il device nuovamente disponibile per una nuova attivazione.

PATCH/devices/{deviceId}

Body

{
  "device_pairing_code": null
}

Questa operazione è tipicamente eseguita dal backoffice, non dal device. Casi d'uso: sostituzione miniPC guasto, riassegnazione device a un altro punto vendita.

4. Manifest

Il manifest contiene TUTTA la programmazione del device: monitor configurati, schedule rules, contenuti con URL firmati, e playlist. Il device lo scarica periodicamente (ogni 30s) e lo usa per determinare cosa mostrare.

4.1 Scarica manifest

GET/manifest/{deviceId}

Risposta — 200 OK

{
  "device_id": 42,
  "device_name": "Ristorante Roma 1",
  "generated_at": "2026-03-01T12:00:00.000Z",    // timestamp generazione

  "monitors": [
    {
      "id": 101,
      "monitor_index": 0,                         // 0, 1, 2
      "monitor_name": "Sala",
      "schedule_id": 5,
      "schedule_timezone": "Europe/Rome",
      "rules": [                                   // regole della schedule
        {
          "id": 201,
          "priority": 1,                           // più basso = più prioritario
          "rule_type": "weekly",                   // "weekly" | "date"
          "day_of_week": 1,                        // 1=Lun..7=Dom (null se date)
          "date_ymd": null,                        // "YYYY-MM-DD" (null se weekly)
          "start_time": "08:00:00",
          "end_time": "22:00:00",
          "target_type": "content",                // "content" | "playlist"
          "target_id": 301,
          "is_default": false
        },
        {
          "id": 202,
          "priority": 999,
          "rule_type": "weekly",
          "day_of_week": null,
          "date_ymd": null,
          "start_time": "00:00:00",
          "end_time": "23:59:59",
          "target_type": "content",
          "target_id": 302,
          "is_default": true                       // regola di fallback
        }
      ],
      "fallback_content_id": 302,                  // contenuto se nessuna regola attiva
      "override_target_type": "none",              // "none" | "content" | "playlist"
      "override_target_id": null,
      "override_until": null                       // ISO datetime o null
    }
  ],

  "contents": [
    {
      "id": 301,
      "content_name": "promo-estate.mp4",
      "content_kind": "video",                     // "img" | "video" | "gif"
      "checksum_sha256": "a1b2c3...",              // per verifica integrità
      "file_size_bytes": 15400000,
      "mime_type": "video/mp4",
      "duration_seconds": 30,                      // null per immagini
      "signed_url": "https://storage.../promo.mp4?token=..."
    }
  ],

  "playlists": [
    {
      "id": 401,
      "playlist_name": "Menu pranzo",
      "loop_enabled": true,
      "items": [
        {
          "content_id": 301,
          "position": 0,
          "duration_override_seconds": 15           // null = usa durata default
        }
      ]
    }
  ]
}

Errori possibili

StatusCausaAzione device
404Device non trovato o eliminatoFermare polling, mostrare errore
500Errore serverUsare ultimo manifest in cache, riprova al prossimo ciclo

Strategia di caching

Il device deve confrontare generated_at con il valore precedente. Se identico, il manifest non è cambiato e non serve riscaricare contenuti. Le signed_url scadono dopo 1 ora — il device deve usarle per scaricare i file, poi servire i contenuti dal disco locale.

5. Download contenuti

Dopo aver ricevuto il manifest, il device deve scaricare tutti i contenuti referenziati. I file vanno salvati su disco locale e serviti da lì durante il rendering.

5.1 Flusso di download

Per ogni content nel manifest.contents:
  1. Controlla se il file esiste già in cache locale
     (confronta checksum_sha256 con il file su disco)
  2. Se NON esiste o checksum diverso → scarica da signed_url
  3. Verifica integrità: calcola SHA-256 del file scaricato
     e confronta con checksum_sha256 del manifest
  4. Salva su disco con naming: {content_id}_{checksum[:8]}.{ext}
  5. Invia sync-ack al server (sezione 5)
  6. Rimuovi file orfani non più nel manifest

5.2 URL firmati per download diretto

Le signed_url nel manifest sono già pronte per il download (GET diretto). Se servisse rigenerare un URL scaduto per un singolo contenuto:

GET/content/{contentId}/url

Risposta — 200 OK

{
  "signed_url": "https://storage.supabase.co/.../file.mp4?token=..."
}

Errori possibili

StatusCausa
404Contenuto non trovato o eliminato
500Errore generazione URL firmato

Nota

Le URL firmate scadono dopo 1 ora. Il device non deve salvare le URL ma solo i file scaricati. Alla prossima poll del manifest riceverà URL aggiornate.

6. Sync Acknowledgement

Dopo aver scaricato (o tentato di scaricare) un contenuto, il device invia una conferma al server. Questo permette al backoffice di sapere lo stato di sync di ogni device.

POST/sync-ack

Body richiesto

{
  "device_id": 42,                               // obbligatorio
  "content_id": 301,                             // obbligatorio
  "download_state": "ready",                     // vedi tabella sotto
  "local_path": "/cache/301_a1b2c3d4.mp4",      // opzionale — path su disco
  "last_error": null                             // opzionale — messaggio errore
}

Valori validi per download_state

StatoSignificatoQuando usare
pendingDownload pianificatoFile in coda per il download
downloadingDownload in corsoDownload avviato
readyFile prontoDownload completato e checksum verificato
failedDownload fallitoErrore di rete, checksum non valido, disco pieno
deletedFile rimossoFile non più necessario e cancellato dal disco

Risposta — 200 OK

{ "ok": true }

Errori possibili

StatusCausa
400device_id o content_id mancante
500Errore database

Comportamento upsert

Il server usa upsert su (device_id, content_id). Chiamate ripetute per lo stesso contenuto aggiornano lo stato esistente. Ogni sync-ack aggiorna anche last_sync_at su tutti i monitor del device.

7. Heartbeat

Il device invia un heartbeat ogni 30 secondi per segnalare che è vivo e funzionante. Il backoffice usa il timestamp dell'ultimo heartbeat per determinare se il device è online (soglia: 90 secondi).

POST/heartbeat

Body richiesto

{
  "device_id": 42,                              // obbligatorio
  "uptime_seconds": 86400,                      // secondi dall'avvio del player
  "storage_free_bytes": 5368709120,              // spazio libero su disco
  "storage_total_bytes": 53687091200,            // spazio totale disco
  "player_version": "1.2.0",                    // versione software player
  "internet_ok": true,                           // connettività internet
  "server_ok": true,                             // raggiungibilità server API
  "redis_ok": true,                              // se applicabile
  "sync_ok": true,                               // ultimo sync riuscito
  "error_codes_json": null                       // array di codici errore o null
}

Tutti i campi tranne device_id sono opzionali. Si consiglia comunque di inviare sempre almeno uptime_seconds,player_version e i flag di stato.

Esempio con errori attivi

{
  "device_id": 42,
  "uptime_seconds": 3600,
  "player_version": "1.2.0",
  "internet_ok": true,
  "server_ok": true,
  "sync_ok": false,
  "error_codes_json": ["SYNC_FAILED", "STORAGE_LOW"]
}

Risposta — 200 OK

{ "ok": true }

Errori possibili

StatusCausaAzione device
400device_id mancanteBug nel player — fix e riavvia
500Errore databaseIgnorare, riprovare al prossimo ciclo

Effetti collaterali

Oltre a inserire il record heartbeat, il server aggiorna last_heartbeat_at eplayer_version sulla tabella device.

8. Comandi remoti

Il backoffice può inviare comandi al device. Il device li riceve facendo polling ogni 10 secondi, li esegue, e conferma l'esecuzione.

8.1 Poll comandi in attesa

GET/commands/{deviceId}

Restituisce tutti i comandi con status "queued", ordinati per data di richiesta (più vecchi prima).

Risposta — 200 OK

{
  "commands": [
    {
      "id": 501,
      "device_id": 42,
      "command": "force_sync",
      "params_json": null,
      "status": "queued",
      "requested_by_user_id": null,
      "requested_at": "2026-03-01T12:00:00.000Z",
      "acked_at": null
    }
  ]
}

8.2 Tipi di comandi

ComandoDescrizioneAzione device
rebootRiavvia il sistemaRiavviare il miniPC (sudo reboot)
restart_playerRiavvia solo il playerRiavviare il processo player senza reboot OS
force_syncForza riscaricamento contenutiRiscaricare manifest + tutti i contenuti
refresh_configAggiorna configurazioneRiscaricare manifest immediatamente
set_overrideImposta override su un monitorApplicare override dai params_json

Esempio params_json per set_override

{
  "monitor_id": 101,
  "target_type": "content",
  "target_id": 305,
  "until": "2026-03-01T18:00:00.000Z"
}

8.3 Conferma esecuzione comando

PATCH/commands/{deviceId}

Dopo aver eseguito (o tentato di eseguire) un comando, il device deve confermarlo.

Body richiesto

{
  "command_id": 501,                // obbligatorio — ID del comando
  "status": "ack"                   // "ack" (successo) o "failed" (fallito)
}

Risposta — 200 OK

{
  "id": 501,
  "device_id": 42,
  "command": "force_sync",
  "status": "ack",
  "acked_at": "2026-03-01T12:00:05.000Z",
  ...
}

Errori possibili

StatusCausa
400command_id o status mancante
500Errore database

Flusso completo di un comando

Backoffice: POST /commands/{deviceId}  → status: "queued"
Device:     GET  /commands/{deviceId}  → riceve comando
Device:     esegue il comando
Device:     PATCH /commands/{deviceId} → status: "ack" o "failed"

9. Risoluzione schedule (logica client)

Il device deve determinare quale contenuto mostrare su ogni monitor in ogni momento. Questa logica gira interamente sul device, usando le regole ricevute nel manifest.

9.1 Ordine di priorità

Per ogni monitor, in questo ordine:

1. OVERRIDE ATTIVO
   Se override_target_type != "none"
   E override_target_id != null
   E (override_until è null OPPURE override_until > now)
   → Mostra override_target_id

2. DATE RULES (regole per data specifica)
   Filtra rules dove:
     rule_type == "date"
     date_ymd == oggi (YYYY-MM-DD)
     ora corrente è dentro [start_time, end_time)
   Ordina per priority (più basso = più prioritario)
   → Usa la prima match

3. WEEKLY RULES (regole settimanali)
   Filtra rules dove:
     rule_type == "weekly"
     day_of_week == giorno ISO corrente (1=Lun..7=Dom)
     ora corrente è dentro [start_time, end_time)
   Ordina per priority
   → Usa la prima match

4. DEFAULT RULE
   Filtra rules dove is_default == true
   → Usa questa

5. FALLBACK CONTENT
   Se nessuna regola attiva → mostra fallback_content_id

6. NESSUN CONTENUTO
   Se anche fallback è null → schermo nero / logo

9.2 Confronto orari

Gli orari sono in formato HH:MM:SS. Il confronto è lessicografico (string-based). Le fasce notturne sono supportate:

Fascia diurna (start <= end):
  "08:00:00" - "22:00:00"
  → attiva se ora >= start AND ora < end

Fascia notturna (start > end):
  "22:00:00" - "06:00:00"
  → attiva se ora >= start OR ora < end

9.3 Conversione giorno della settimana

JavaScript: 0=Dom, 1=Lun, 2=Mar, ..., 6=Sab
ISO 8601:   1=Lun, 2=Mar, ..., 7=Dom

Conversione: iso = (js === 0) ? 7 : js

9.4 Rendering playlist

Quando il target è una playlist, il device deve ciclare gli items in ordine di position:

Per ogni item nella playlist:
  1. Mostra il contenuto (content_id)
  2. Durata:
     - Se duration_override_seconds != null → usa quello
     - Se content.duration_seconds != null → usa quello (video)
     - Altrimenti → usa DEFAULT_IMAGE_DURATION_SECONDS (10s)
  3. Per i video: aspetta l'evento "ended" poi avanza
  4. Se loop_enabled == true: ricomincia da capo dopo l'ultimo item
  5. Se loop_enabled == false: resta sull'ultimo item

10. Codici errore

Il device può segnalare errori attivi nell'heartbeat tramite il campo error_codes_json. Questi codici vengono visualizzati nel backoffice per diagnostica.

CodiceSignificatoQuando segnalarlo
NO_INTERNETNessuna connessione internetPing a 8.8.8.8 fallisce
NO_SERVERServer API non raggiungibileManifest poll fallisce con errore di rete
SYNC_FAILEDSync contenuti fallitoDownload di uno o più file fallito
STORAGE_LOWSpazio disco insufficienteSpazio libero < 500MB (o soglia configurata)
REDIS_DOWNRedis non raggiungibileSe il device usa Redis locale per cache
EMPTY_SCHEDULENessuna regola attivaNessun contenuto da mostrare su almeno un monitor

Visualizzazione errori

Gli errori non devono interrompere la visualizzazione dei contenuti. Il player deve continuare a mostrare l'ultimo contenuto valido in cache. Gli errori vanno mostrati come indicatori discreti (piccole icone semitrasparenti) visibili solo a un tecnico, non al pubblico in sala.

11. Costanti e intervalli

CostanteValoreDescrizione
HEARTBEAT_INTERVAL_MS30.000 (30s)Intervallo invio heartbeat
MANIFEST_POLL_INTERVAL_MS30.000 (30s)Intervallo polling manifest
COMMAND_POLL_INTERVAL_MS10.000 (10s)Intervallo polling comandi
DEVICE_ONLINE_THRESHOLD_SECONDS90Soglia per considerare device online
DEFAULT_IMAGE_DURATION_SECONDS10Durata default immagini/gif in playlist
SIGNED_URL_EXPIRY3.600 (1h)Validità URL firmati per download

12. Ciclo di vita completo

Diagramma completo del comportamento del software player, dalla prima accensione al funzionamento continuo.

┌─────────────────────────────────────────────────────────────┐
│                     PRIMA ACCENSIONE                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Esiste device_id salvato su disco?                         │
│  ├─ NO  → Avvia provisioning                               │
│  │        POST /provisioning/claim                          │
│  │        Salva device_id + device_token                    │
│  │                                                          │
│  └─ SÌ  → Continua                                         │
│                                                             │
│  Device già reclamato (device_pairing_code != null)?        │
│  ├─ NO  → Reclama con MAC address                          │
│  │        PATCH /devices/{deviceId}                         │
│  │        Body: { device_pairing_code: "AA:BB:CC:..." }    │
│  │                                                          │
│  └─ SÌ  → Vai al ciclo principale                          │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                    CICLO PRINCIPALE                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──── Ogni 30s ────────────────────────────────────┐       │
│  │ MANIFEST POLL                                     │       │
│  │ GET /manifest/{deviceId}                          │       │
│  │ ├─ 200 → Confronta generated_at                   │       │
│  │ │        Se cambiato → Content Sync               │       │
│  │ │        Se uguale  → skip                        │       │
│  │ ├─ 404 → Device eliminato, ferma tutto            │       │
│  │ └─ 5xx → Usa cache locale, segnala NO_SERVER      │       │
│  └───────────────────────────────────────────────────┘       │
│                                                             │
│  ┌──── Content Sync (quando manifest cambia) ───────┐       │
│  │ Per ogni content in manifest.contents:             │       │
│  │ 1. File in cache con stesso checksum? → skip      │       │
│  │ 2. Download da signed_url                         │       │
│  │ 3. Verifica SHA-256                               │       │
│  │ 4. POST /sync-ack (ready / failed)                │       │
│  │ 5. Rimuovi file orfani dal disco                  │       │
│  └───────────────────────────────────────────────────┘       │
│                                                             │
│  ┌──── Ogni 30s ────────────────────────────────────┐       │
│  │ HEARTBEAT                                         │       │
│  │ POST /heartbeat                                   │       │
│  │ Body: device_id, uptime, storage, version,        │       │
│  │       health flags, error_codes                   │       │
│  └───────────────────────────────────────────────────┘       │
│                                                             │
│  ┌──── Ogni 10s ────────────────────────────────────┐       │
│  │ COMMAND POLL                                      │       │
│  │ GET /commands/{deviceId}                          │       │
│  │ Per ogni comando:                                 │       │
│  │   1. Esegui (reboot/sync/restart/override)        │       │
│  │   2. PATCH /commands/{deviceId} → ack/failed      │       │
│  └───────────────────────────────────────────────────┘       │
│                                                             │
│  ┌──── Ogni secondo ────────────────────────────────┐       │
│  │ RENDER                                            │       │
│  │ Per ogni monitor:                                 │       │
│  │   1. Override attivo? → mostra override           │       │
│  │   2. Date rule match? → mostra quel contenuto     │       │
│  │   3. Weekly rule match? → mostra quel contenuto   │       │
│  │   4. Default rule? → mostra default               │       │
│  │   5. Fallback content? → mostra fallback          │       │
│  │   6. Niente → schermo nero                        │       │
│  └───────────────────────────────────────────────────┘       │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                    GESTIONE ERRORI                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  • MAI interrompere la visualizzazione per un errore        │
│  • Contenuti in cache → continuare a mostrare               │
│  • Errori → icone discrete in basso a destra                │
│  • Segnalare errori nell'heartbeat                          │
│  • Server irraggiungibile → usare ultimo manifest valido    │
│  • Riprovare automaticamente al prossimo ciclo              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Naming file su disco

Convenzione consigliata per i file in cache: {content_id}_{checksum_sha256.slice(0,8)}.{ext}. Esempio: 301_a1b2c3d4.mp4. Questo permette di identificare rapidamente file obsoleti (content_id presente ma checksum diverso).

CHECKMATE API v1 — Ultimo aggiornamento: Marzo 2026