From 498f6069d0e75c268a8cdac9869bfd62f6b36723 Mon Sep 17 00:00:00 2001 From: Emiliano Frascaro Date: Sun, 14 Jun 2026 21:26:35 +0200 Subject: [PATCH] Documenta l'architettura completa del sistema. Aggiunge docs/ARCHITECTURE.md con topologia rete, edge nginx, flussi HLS/replay e monitoraggio ops; allinea i documenti esistenti. Co-authored-by: Cursor --- README.md | 9 +- docs/ARCHITECTURE.md | 415 +++++++++++++++++++++++ docs/LIVE_STREAMING.md | 4 +- docs/REPLAY_MODULE.md | 5 +- docs/infrastructure/SERVER_DEPLOYMENT.md | 17 +- 5 files changed, 436 insertions(+), 14 deletions(-) create mode 100644 docs/ARCHITECTURE.md diff --git a/README.md b/README.md index 3c9a1f8..f91fcbe 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,13 @@ Vedi [native/android/README.md](native/android/README.md). ## Architettura -- Video (MVP): Phone → MediaMTX (RTMP) → HLS su `matchlivetv.*` (pagina `/live/:session_id`) -- Video (premium): stesso ingest + relay ffmpeg verso YouTube +Documentazione completa: **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)** (rete, edge nginx, flussi video, ops). + +In sintesi: + +- Video live: Phone → MediaMTX (RTMP) → HLS via edge `/hls/` (non Puma) +- Video replay: Rails auth → redirect `/media/` → Garage (non Puma) +- Video premium: stesso ingest + relay ffmpeg verso YouTube - Controllo: Phone ↔ Action Cable ↔ Rails ↔ PostgreSQL - Disconnessioni: MediaMTX `alwaysAvailable` slate + timeout 5 min (Sidekiq) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..6a2d101 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,415 @@ +# Architettura — Match Live TV + +Documento di riferimento per l'intero sistema: rete, container Docker, flussi video, replay, monitoraggio ops e rilasci. + +**Ultimo aggiornamento:** 2026-06-14 +**Produzione:** `eminux@192.168.1.146` → `/opt/matchlivetv` + +--- + +## Indice + +1. [Panoramica](#panoramica) +2. [Topologia di rete](#topologia-di-rete) +3. [Stack Docker (produzione)](#stack-docker-produzione) +4. [Proxy edge (nginx)](#proxy-edge-nginx) +5. [Diretta live](#diretta-live) +6. [Replay e storage](#replay-e-storage) +7. [Controllo applicativo](#controllo-applicativo) +8. [Monitoraggio ops](#monitoraggio-ops) +9. [Rilasci e downtime](#rilasci-e-downtime) +10. [Sviluppo locale vs produzione](#sviluppo-locale-vs-produzione) +11. [Documentazione correlata](#documentazione-correlata) + +--- + +## Panoramica + +Match Live TV è una piattaforma mobile-first per streaming di partite sportive giovanili. + +| Area | Tecnologia | +|------|------------| +| Backend | Rails 7.2 (API REST, Action Cable, admin, pagine pubbliche) | +| Job asincroni | Sidekiq + Redis | +| Database | PostgreSQL 16 | +| Ingest video | MediaMTX (RTMP :1935) | +| Playback live | HLS (MediaMTX :8888) | +| Replay | Garage (S3-compatible) + MP4 | +| App mobile | Android nativo (Kotlin + Compose, RTMP) | +| Edge LAN | Nginx (`edge`) sulla porta host **3000** | +| HTTPS pubblico | Nginx Proxy Manager (NPM) → `192.168.1.146:3000` | + +**Principio guida:** il traffico video pesante (HLS live, MP4 replay) **non passa da Puma**. Rails gestisce autenticazione, logica di business e redirect verso storage/streaming; nginx edge e MediaMTX servono i byte. + +--- + +## Topologia di rete + +``` + Internet + │ + ┌──────────────────┼──────────────────┐ + │ │ │ + Router :443 Router :1935 (opz. :8888) + │ │ │ + ▼ ▼ ▼ + Nginx Proxy Manager MediaMTX RTMP MediaMTX HLS + (LAN, TLS) (host :1935) (host :8888, opz.) + │ + │ forward http://192.168.1.146:3000 + ▼ + ┌──────────────────────────────────────────────────────────┐ + │ Server 192.168.1.146 — Docker Compose (rete interna) │ + │ │ + │ ┌─────────┐ ┌─────────┐ ┌──────────┐ │ + │ │ edge │────►│ rails │────►│ postgres │ │ + │ │ nginx │ │ Puma │ └──────────┘ │ + │ │ :3000 │ └────┬────┘ ┌──────────┐ │ + │ └────┬────┘ │ │ redis │ │ + │ │ │ └──────────┘ │ + │ │ ┌────▼────┐ ┌──────────┐ │ + │ │ │ sidekiq │────►│ garage │ │ + │ │ └────┬────┘ │ (S3) │ │ + │ │ │ └──────────┘ │ + │ ├───────────────┼────────► mediamtx :8888/:1935 │ + │ └───────────────┴────────► garage :3900 │ + │ │ + │ ntfy :8090 (LAN) — notifiche push ops │ + └──────────────────────────────────────────────────────────┘ + │ + ▼ + Telefono Android ──RTMP──► :1935 (WAN) +``` + +### Porte + +| Porta | Esposizione | Servizio | Note | +|-------|-------------|----------|------| +| **443** | NPM (altro host LAN) | HTTPS sito | Forward a `:3000` (edge) | +| **1935** | WAN → server | RTMP ingest | Obbligatoria per streaming da 4G | +| **3000** | Solo LAN | edge nginx | Punto di ingresso NPM | +| **8090** | Solo LAN | ntfy | Proxy HTTPS opzionale (`ntfy.matchlivetv.it`) | +| **8888** | Opzionale WAN | HLS MediaMTX | Di solito non serve: HLS passa da NPM → edge → `/hls/` | +| 5432, 6379, 3900, 9997 | Solo Docker | DB, Redis, Garage API, MediaMTX API | Mai su WAN | + +--- + +## Stack Docker (produzione) + +File: [`infra/docker-compose.prod.yml`](../infra/docker-compose.prod.yml) + +| Servizio | Immagine / build | Ruolo | +|----------|------------------|-------| +| **postgres** | `postgres:16-alpine` | Dati applicativi | +| **redis** | `redis:7-alpine` | Sidekiq, cache, campioni latenza ops | +| **mediamtx** | `bluenviron/mediamtx` | RTMP ingest, HLS, API path dinamici | +| **garage** | `dxflrs/garage` | Object storage S3 per replay MP4/thumbnail | +| **rails** | build `backend/` | API, web, Action Cable, Puma | +| **sidekiq** | build `backend/` | Job, relay ffmpeg (YouTube, overlay), ops | +| **edge** | `nginx:alpine` | Reverse proxy LAN, routing video, pagina cortesia | +| **ntfy** | `binwiederhier/ntfy` | Push ops su telefono | + +### Volumi persistenti + +Root dati: `${MATCHLIVETV_VIDEOS_ROOT}` (produzione: `/media/videos/matchlivetv`) + +| Path host | Contenuto | +|-----------|-----------| +| `.../recordings` | Segmenti registrazione MediaMTX, staging replay | +| `.../active_storage` | Allegati Rails (loghi società, ecc.) | +| `.../log` | Log applicativi + cron (`cron-replay.log`, `cron-ops.log`) | +| `.../garage/meta`, `.../garage/data` | Metadati e blob Garage | +| `postgres_data`, `redis_data`, `ntfy_data` | Volumi Docker nominati | + +### Puma + +`RAILS_MAX_THREADS=5` in produzione. I thread Puma sono riservati ad API, pagine HTML, WebSocket e auth — non al proxy di flussi video lunghi. + +--- + +## Proxy edge (nginx) + +File: [`infra/nginx-edge/nginx.conf`](../infra/nginx-edge/nginx.conf) + +NPM termina TLS e inoltra **tutto** il traffico HTTP a `edge:80` (host `:3000`). Edge smista per path: + +| Location | Destinazione | Passa da Puma? | +|----------|--------------|----------------| +| `/hls/` | `mediamtx:8888` | **No** | +| `/media/` | `garage:3900` | **No** | +| `/up`, `/up/deep` | `rails:3000` | Sì (pass-through, no cortesia) | +| `/cable` | `rails:3000` | Sì (WebSocket) | +| `/` | `rails:3000` | Sì (con pagina cortesia su 502/503/504) | + +### HLS live (`/hls/`) + +Introdotto per evitare saturazione Puma durante dirette con molti spettatori (migliaia di richieste segmenti `.ts` / `.m3u8`). + +``` +Browser ──► NPM ──► edge /hls/live/match_{uuid}/index.m3u8 + │ rewrite → /live/match_{uuid}/index.m3u8 + └──► mediamtx:8888 +``` + +- Cookie MediaMTX: se il browser non invia cookie, edge aggiunge `cookieCheck=1` (map nginx). +- `proxy_buffering off`, `proxy_read_timeout 3600s` per streaming lungo. +- URL pubblico: `https://www.matchlivetv.it/hls/live/match_{uuid}/index.m3u8` (`HLS_PUBLIC_URL`). + +**NPM:** non serve più una custom location `/hls` verso `:8888`. Un unico Proxy Host verso `:3000` è sufficiente; edge instrada internamente. + +**Fallback dev:** `HlsProxyController` (`/hls/*` su Rails) resta per sviluppo locale senza container `edge`. + +### Replay media (`/media/`) + +Introdotto per evitare che Puma faccia da proxy dei file MP4 (decine/hundreds di MB per richiesta). + +``` +Browser ──► /replay/:id/stream + │ + ▼ + Rails: auth + Recordings::PublicMediaUrl + │ + ▼ 302 redirect + /media/replays/...?X-Amz-Signature=... + │ + ▼ + edge ──► garage:3900 (Host: garage:3900, Range support) +``` + +- `REPLAY_MEDIA_REDIRECT=true` (default produzione). +- Rails genera URL presigned S3 sull'endpoint interno e lo riscrive su `APP_PUBLIC_URL/media/...`. +- Fallback senza Garage: `Recordings::ServeFromStorage` (proxy Rails, solo dev/legacy). + +### Pagina di cortesia + +Durante restart Rails, `edge` risponde con HTML statico (`@maintenance`) invece di lasciare NPM con 502. Eccezioni: `/up`, `/up/deep` (monitoraggio ops). + +--- + +## Diretta live + +Vedi anche [`LIVE_STREAMING.md`](LIVE_STREAMING.md) per pause, overlay e player HLS.js. + +### Flusso ingest + +``` +App Android (RTMP 720p, AAC mono) + │ + ▼ +MediaMTX :1935 — path live/match_{uuid} (grezzo, telefono) + │ + ▼ overlay ffmpeg (Streams::OverlayRelay) +path live/match_{uuid}_air (tabellone + badge bruciati nel video) + │ + ├──► HLS :8888 ──► edge /hls/ ──► spettatori web + └──► ffmpeg -c copy ──► YouTube RTMP (Streams::YoutubeRelay, Premium) +``` + +| Componente | Ruolo | +|------------|--------| +| `Mediamtx::PathManager` | Crea path dinamici via API :9997 | +| `alwaysAvailable` + slate | Copertina `/slates/offline.mp4` senza interrompere HLS | +| `Streams::OverlayRelay` | Ricodifica con PNG 1280×720 aggiornata ogni ~2s | +| `Streams::YoutubeRelay` | Legge HLS da `mediamtx:8888` (non più via Rails) | +| `Mediamtx::PublisherSync` | Stato publisher online/offline in Rails | + +### Pausa e continuità + +1. App chiama `PATCH /sessions/:id/pause` → stop RTMP. +2. MediaMTX passa alla slate sullo **stesso path** (stesso URL HLS). +3. YouTube relay continua sulla stessa uscita (vede slate). +4. Ripresa: `resume` → app ripubblica RTMP; MediaMTX concatena camera senza nuovo URL. + +**Recording inline MediaMTX disabilitato** (`record: false` su path live): il recorder al switch slate→camera distruggeva il muxer HLS. I replay usano job dedicato (vedi sotto). + +### Pagina live web + +- URL: `/live/:session_id` +- Player: HLS.js su `HLS_PUBLIC_URL/live/match_{uuid}_air/index.m3u8` +- Stato: `GET /live/:id/status.json` (badge, recovery buffer, no-store) + +--- + +## Replay e storage + +Vedi [`REPLAY_MODULE.md`](REPLAY_MODULE.md) per piani, retention, cron e API. + +### Pipeline post-diretta + +``` +Sessions::Stop → FinalizeSession → UploadJob + │ + ▼ +Garage bucket matchlivetv-replays (MP4 + thumbnail) + │ + ├──► Email «replay pronto» + ├──► /replay/:id (player web) + └──► PublishToYoutubeJob (Premium Full, se diretta era su YouTube) +``` + +### Delivery al browser + +| Endpoint Rails | Azione | +|----------------|--------| +| `GET /replay/:id` | Pagina player | +| `GET /replay/:id/stream` | Auth → redirect 302 → `/media/...` | +| `GET /replay/:id/thumbnail` | Auth → redirect 302 → `/media/...` | +| `GET /replay/:id/download` | Auth premium → redirect attachment | + +### YouTube — doppia presenza (Premium Full) + +| Tab / canale | Contenuto | +|--------------|-----------| +| **Live** (diretta YouTube) | Archivio stream live sul canale | +| **Video** (VOD) | Republicazione MP4 automatica via `PublishToYoutubeJob` | + +--- + +## Controllo applicativo + +``` +App Android ◄──── REST API (/api/v1/...) ────► Rails + │ │ + │ Action Cable (/cable) │ + └──────────────────────────────────────────────┘ + │ + ▼ + PostgreSQL + │ + Sidekiq ◄── Redis ◄──────┘ + │ + ├── HealthMonitorJob, UploadJob, overlay refresh + ├── YoutubeRelay, OverlayRelay (ffmpeg) + └── PublishToYoutubeJob, purge replay, ecc. +``` + +| Canale | Uso | +|--------|-----| +| REST JWT | Login, sessioni, punteggio, configurazione stream | +| Action Cable | Tabellone live, aggiornamenti regia (in sviluppo su app) | +| Webhook MediaMTX | Eventi publisher connect/disconnect | + +--- + +## Monitoraggio ops + +Vedi [`OPS_MONITORING.md`](OPS_MONITORING.md) per setup ntfy e troubleshooting. + +### Componenti + +| Pezzo | Ruolo | +|-------|--------| +| `Ops::HealthMonitorJob` | Sidekiq, ogni ~3 min | +| `Ops::HealthChecks` | Esegue tutti i check, apre/chiude incidenti | +| `Ops::UpLatencyTracker` | Campioni latenza in Redis, calcolo **p95** | +| `Ops::LogScanner` | Cron ogni 10 min su log Docker | +| `Ops::Incident` + `/admin/ops` | Dashboard e storico | +| `GET /up/deep` | JSON esteso (token `X-Ops-Token`) | + +### Check HTTP (evoluzione 2026-06) + +| Kind | Severità | Cosa verifica | Frequenza | +|------|----------|---------------|-----------| +| `http_rails` | **critical** | `GET http://edge/up` (rete Docker, no hairpin NPM) | Ogni ciclo (~3 min) | +| `rails_latency` | warning / critical | p95 latenza campioni `http_rails` | Ogni ciclo (dopo ≥5 campioni) | +| `http_public` | **warning** | `GET https://www.matchlivetv.it/up` | Ogni 15 min (`OPS_HTTP_PUBLIC_INTERVAL_SECS`) | + +**Perché la separazione:** il check dall'interno del container Rails verso l'URL pubblico può fallire per hairpin NAT / timeout NPM anche quando il sito è raggiungibile dall'esterno. `http_rails` misura la salute reale dello stack; `http_public` è un controllo complementare non bloccante. + +### Soglie latenza p95 + +| Variabile | Default | +|-----------|---------| +| `OPS_UP_LATENCY_WARN_MS` | 2000 | +| `OPS_UP_LATENCY_CRIT_MS` | 5000 | +| `OPS_UP_LATENCY_SAMPLES` | 60 | +| `OPS_UP_LATENCY_MIN_SAMPLES` | 5 | + +`http_rails` registra ogni misura in Redis; `check_rails_latency` calcola p95 e apre incidente `rails_latency` se supera soglia. + +### Altri check + +Disco root, dimensione archivio registrazioni, PostgreSQL, Redis, MediaMTX API, Garage head-bucket, heartbeat Sidekiq, dead queue Sidekiq. + +### Notifiche + +Default `OPS_NOTIFY_SEVERITIES=critical` → `http_public` (warning) e `rails_latency` (warning) **non** notificano; critical sì (ntfy + email opzionale). + +--- + +## Rilasci e downtime + +Script: [`scripts/deploy/release_production.sh`](../scripts/deploy/release_production.sh) + +``` +Dev machine Server produzione + │ │ + │ rsync (sync_to_server.sh) │ + ├─────────────────────────────────────►│ + │ │ + │ build rails + sidekiq + │ migrate DB + │ force-recreate edge + │ recreate rails (+ wait /up) + │ recreate sidekiq + │ install cron +``` + +| Fase | Impatto utente | +|------|----------------| +| `edge` recreate | Nessuno (nginx torna subito) | +| `rails` recreate | Pagina cortesia ~30–90s; `/up` brevemente down | +| RTMP / diretta attiva | **Non interrotta** (MediaMTX indipendente) | +| Replay in riproduzione | Continuano via `/media/` → Garage (edge resta su) | + +Smoke test post-rilascio: + +```bash +curl -sf https://www.matchlivetv.it/up +curl -sI https://www.matchlivetv.it/hls/ # 404 MediaMTX = routing OK +docker compose exec rails bundle exec rails runner 'puts Ops::HealthChecks.new.summary[:status]' +``` + +--- + +## Sviluppo locale vs produzione + +| Aspetto | Dev (`docker compose`) | Produzione | +|---------|------------------------|------------| +| Edge nginx | Assente (NPM simulato con porta 3000 → rails) | Container `edge` obbligatorio | +| HLS playback | `HlsProxyController` su Rails | edge → MediaMTX | +| Replay stream | Redirect o proxy Rails | Redirect → edge → Garage | +| Codice Rails | Volume mount hot-reload | Immagine Docker buildata | +| MediaMTX :1935/:8888 | Esposti su localhost | :1935 WAN, :8888 opzionale | + +Avvio locale: + +```bash +cd infra && cp .env.example .env && docker compose up -d --build +``` + +--- + +## Documentazione correlata + +| Documento | Contenuto | +|-----------|-----------| +| [`infrastructure/SERVER_DEPLOYMENT.md`](infrastructure/SERVER_DEPLOYMENT.md) | Bootstrap server, NPM, cron, backup | +| [`LIVE_STREAMING.md`](LIVE_STREAMING.md) | MediaMTX, pause, overlay, HLS.js | +| [`REPLAY_MODULE.md`](REPLAY_MODULE.md) | Garage, retention, YouTube VOD | +| [`OPS_MONITORING.md`](OPS_MONITORING.md) | ntfy, variabili ops, troubleshooting | +| [`TABELLONI_E_OVERLAY.md`](TABELLONI_E_OVERLAY.md) | Grafica in stream | +| [`PRODUCT_PREMIUM.md`](PRODUCT_PREMIUM.md) | Piani e funzionalità premium | +| [`native/android/README.md`](../native/android/README.md) | App Android | + +--- + +## Cronologia modifiche architetturali rilevanti + +| Data | Modifica | +|------|----------| +| 2026-05 | Introduzione container `edge` + pagina cortesia ai rilasci | +| 2026-06 | Replay: redirect 302 a `/media/` → Garage (fuori da Puma) | +| 2026-06 | HLS live: edge → MediaMTX (fuori da Puma) | +| 2026-06 | Ops: check `http_rails` + `http_public` separati, latenza p95 `UpLatencyTracker` | +| 2026-06 | `RAILS_MAX_THREADS=5`; relay YouTube legge `mediamtx:8888` diretto | diff --git a/docs/LIVE_STREAMING.md b/docs/LIVE_STREAMING.md index e2769c3..f894f78 100644 --- a/docs/LIVE_STREAMING.md +++ b/docs/LIVE_STREAMING.md @@ -18,7 +18,7 @@ Il telefono **non** invia la copertina: risparmia banda; lo switch è lato serve | MediaMTX | Path dinamico, `alwaysAvailable` + slate | | `Streams::YoutubeRelay` | Relay continuo verso YouTube (no hook wget su distroless) | | `Mediamtx::PublisherSync` | Stato Rails da API paths (publisher online) | -| `/hls/...` (Rails proxy) | Player web | +| `/hls/...` (edge → MediaMTX in prod; Rails proxy in dev) | Player web | ## Pausa e continuità @@ -43,7 +43,7 @@ Telefono RTMP ──► MediaMTX path live/match_{uuid} publisher ON ├─► camera (H.264/AAC) publisher OFF└─► alwaysAvailable → /slates/offline.mp4 │ - ├─► HLS (segmenti ~1s) ──► proxy Rails /hls/... ──► player web (HLS.js) + ├─► HLS (segmenti ~1s) ──► edge /hls/ → MediaMTX (prod) o proxy Rails (dev) ──► player web (HLS.js) └─► RTMP lettura ──► Streams::YoutubeRelay (ffmpeg -c copy) ──► YouTube ``` diff --git a/docs/REPLAY_MODULE.md b/docs/REPLAY_MODULE.md index 3916f67..143af65 100644 --- a/docs/REPLAY_MODULE.md +++ b/docs/REPLAY_MODULE.md @@ -29,6 +29,7 @@ MediaMTX registra **solo mentre il telefono pubblica** (RTMP connesso). In pausa ### 4. Riproduzione - Player MP4: `/replay/:id` + stream `/replay/:id/stream` +- Delivery: Rails verifica accesso → redirect 302 a `/media/...` (edge → Garage). Puma non proxya il file. - Contatore visualizzazioni su ogni play (`view_count`) ### 5. Visibilità @@ -124,8 +125,8 @@ bash scripts/garage_set_capacity.sh 85G # oppure senza argomento: calcolo auto | Origine diretta | Copia Garage/S3 | Player sito `/replay/:id` | YouTube VOD | |-----------------|-----------------|----------------------------|-------------| -| `matchlivetv` | Sì (Premium) | MP4 via proxy Rails | Manuale (Premium Full) | -| `youtube` | Sì (Premium) | MP4 via proxy Rails | Auto se Premium Full + diretta YouTube | +| `matchlivetv` | Sì (Premium) | MP4 via redirect `/media/` → Garage | Manuale (Premium Full) | +| `youtube` | Sì (Premium) | MP4 via redirect `/media/` → Garage | Auto se Premium Full + diretta YouTube | Se il file S3 non è disponibile ma esiste `youtube_video_id`, la pagina replay mostra embed YouTube. diff --git a/docs/infrastructure/SERVER_DEPLOYMENT.md b/docs/infrastructure/SERVER_DEPLOYMENT.md index 913d426..6f57d21 100644 --- a/docs/infrastructure/SERVER_DEPLOYMENT.md +++ b/docs/infrastructure/SERVER_DEPLOYMENT.md @@ -2,7 +2,9 @@ **Server:** `eminux@192.168.1.146` (Debian 13) **Path deploy:** `~/matchlivetv` (dopo bootstrap opzionale: `/opt/matchlivetv`) -**Ultimo aggiornamento:** 2026-05-27 +**Ultimo aggiornamento:** 2026-06-14 + +> Architettura completa: [ARCHITECTURE.md](../ARCHITECTURE.md) --- @@ -11,12 +13,15 @@ ``` Internet │ - ├──► Router WAN :443 ──► Nginx Proxy Manager (LAN) ──► http://192.168.1.146:3000 (Rails API + Action Cable) + ├──► Router WAN :443 ──► Nginx Proxy Manager (LAN) ──► http://192.168.1.146:3000 (edge nginx) + │ ├── /hls/ → MediaMTX :8888 + │ ├── /media/ → Garage :3900 + │ └── / → Rails (API + web) │ └──► Router WAN :1935 ──► MediaMTX RTMP (telefoni, ingest video diretto) ``` -Il flusso video **non passa da Rails**: telefono → RTMP :1935 → MediaMTX → relay ffmpeg → YouTube. +Il flusso video live e replay **non passa da Puma**: HLS e MP4 sono serviti da edge verso MediaMTX e Garage; Rails fa auth e redirect. --- @@ -68,11 +73,7 @@ Il flusso video **non passa da Rails**: telefono → RTMP :1935 → MediaMTX → Durante i rilasci Rails viene mostrata una **pagina di cortesia** invece del 502 bianco di NPM. 2. **Websockets:** ON (obbligatorio per `/cable`) -3. **Custom location HLS** (spettatori): - - Location: `/hls` - - Forward: `http://192.168.1.146:8888` (in NPM usare proxy pass che **rimuove** il prefisso `/hls`, così MediaMTX riceve `/live/match_{uuid}/index.m3u8`) - - Scheme: http - - URL pubblico: `https://www.matchlivetv.it/hls/live/match_{uuid}/index.m3u8` +3. **HLS:** nessuna custom location necessaria — edge instrada `/hls/` verso MediaMTX internamente. Un solo Proxy Host verso `:3000` basta. 4. SSL Let's Encrypt su NPM 5. Aggiorna `infra/.env` sul server: - `APP_PUBLIC_URL=https://www.matchlivetv.it`