# Captive Portal CMS — Casa della Scuola

CMS per portali captive: gestione di card informative, gallerie, flip-book e contenuti kiosk a schermo intero, con un'area di amministrazione locale. Stack: Next.js 16 (App Router, Turbopack), React 19, TypeScript, Tailwind v4. Persistenza su file (nessun database). Pensato per girare su **server Ubuntu offline**.

---

## Indice

1. [Avvio](#avvio)
2. [Configurazione (`lib/config.ts`)](#configurazione-libconfigts)
3. [Tipi di card](#tipi-di-card)
4. [File consentiti negli upload](#file-consentiti-negli-upload)
5. [Limiti di testo](#limiti-di-testo)
6. [Sicurezza degli input](#sicurezza-degli-input)
7. [Struttura dei dati (`data/`)](#struttura-dei-dati-data)
8. [Backup e ripristino](#backup-e-ripristino)
9. [Factory Preset (developer)](#factory-preset-developer)
10. [Font](#font)
11. [Prerequisiti di sistema](#prerequisiti-di-sistema)
12. [Risoluzione problemi](#risoluzione-problemi)

---

## Avvio

**Sviluppo:**
```bash
npm install
npm run dev
```
Apri [http://localhost:3000](http://localhost:3000) (portale pubblico) e [http://localhost:3000/admin](http://localhost:3000/admin) (amministrazione).

**Produzione:**
```bash
npm run build
npm start
```

> **Server offline:** la macchina di produzione non ha accesso a internet. NON eseguire `npm install` lì. Installa le dipendenze su una macchina con internet (stesso OS, Linux), poi copia l'intera cartella `node_modules` sul server insieme al progetto buildato. Su Ubuntu basta `npm run build` (se `node_modules` è presente) + `npm start`.

---

## Configurazione (`lib/config.ts`)

Tutte le impostazioni globali sono **flag build-time** nel file [`lib/config.ts`](lib/config.ts). Dopo ogni modifica serve ricostruire: `npm run build`.

| Variabile | Default | Descrizione |
|---|---|---|
| `EXTERNAL_LINK_ENABLED` | `true` | Mostra il tipo di card "External Link" nel menu dell'admin. Le card di quel tipo già esistenti restano comunque visibili e cliccabili anche se messo a `false`. |
| `FACTORY_RESET_ENABLED` | `false` | Mostra la sezione "Factory Preset" nell'admin (funzione developer). Vedi [Factory Preset](#factory-preset-developer). Gli endpoint API restano attivi a prescindere. |
| `DEFAULT_FONT` | `''` | Font di default se il portale non ne ha impostato uno. Stringa vuota = font di sistema (Arial). Altrimenti il nome esatto di un file in `data/fonts/` (es. `"Geist-Variable.woff2"`). |
| `TEXT_LIMITS` | vedi sotto | Limiti caratteri di tutti i campi testuali. |
| `UPLOAD_LIMITS` | vedi sotto | Dimensioni massime upload per famiglia di file. |

### Limiti testo (`TEXT_LIMITS`)
```ts
card: {
  title: 200,
  shortDescription: 500,
  fullContent: 20_000,
  actionUrl: 2000,
},
portal: {
  title: 200,
  welcomeText: 1000,
},
```

### Limiti upload (`UPLOAD_LIMITS`)
```ts
image: 25 MB,
pdf:   20 MB,
video: 1 GB,
```

Per cambiare un qualunque limite: modifica il numero in `lib/config.ts` e ricostruisci. Il valore è condiviso tra interfaccia (contatore / `maxLength`), validazione server e check di upload — un solo punto di verità.

---

## Tipi di card

| Tipo | Nome in admin | Comportamento |
|---|---|---|
| `INFO_PAGE` | Info Page | Pagina informativa: solo cover, niente galleria. |
| `IMAGE_GALLERY` | Image Gallery | Galleria di immagini/video/PDF sfogliabile a schermo intero. |
| `BOOK` | Flip-Book | Sfoglialibro in formato A4 (due pagine affiancate). |
| `FULLSCREEN_LOCK` | Fullscreen Lock (kiosk) | **Takeover totale**: se presente, il portale pubblico mostra SOLO il suo contenuto (immagine o video) a tutto schermo, senza hero, griglia o pulsanti di chiusura. Le altre card vengono nascoste. Utile per redirect/segnaletica kiosk. |
| `EXTERNAL_LINK` | External Link | Apre un URL esterno. Visibile nel menu solo se `EXTERNAL_LINK_ENABLED = true`. |

**Note sulla Fullscreen Lock:**
- Se ci sono più card lock, viene usata la prima per ordine di visualizzazione.
- `/admin` resta sempre accessibile anche con una lock attiva.
- Per tornare al portale normale: elimina (o cambia tipo a) la card lock.

---

## File consentiti negli upload

Gli upload passano per tre controlli in cascata. Se uno fallisce, **nessun file viene salvato** e l'admin riceve un messaggio d'errore.

### 1. Whitelist estensioni
| Famiglia | Estensioni | Limite |
|---|---|---|
| Immagini | `png` `jpg` `jpeg` `gif` `webp` | 25 MB |
| Video | `mp4` `m4v` `webm` `mov` `ogv` `ogg` | 1 GB |
| Documenti | `pdf` | 20 MB |

Tutto il resto (es. `svg`, `heic`, `bmp`, `avi`, `exe`) viene rifiutato. **SVG è escluso di proposito** (può contenere `<script>`).

### 2. Controllo "magic-bytes"
Il contenuto reale del file viene confrontato con l'estensione dichiarata. Esempi rifiutati:
- Un PDF rinominato `.jpg` → rifiutato (contenuto ≠ estensione).
- Un eseguibile rinominato `.png` → rifiutato (tipo non riconosciuto).
- Un PNG rinominato `.jpg` → rifiutato (mismatch interno alla famiglia immagini).

`mov` e `mp4` sono intercambiabili (entrambi container ISO BMFF).

### 3. Transcodifica video automatica
Solo i video possono essere ricodificati. Alla ricezione il server sonda i codec:
- **Video già compatibile** (H.264 + AAC/MP3) → nessuna ricodifica, salvataggio immediato.
- **Video non compatibile** (HEVC iPhone, VP9, AV1, audio Opus/Vorbis…) → messo in coda e ricodificato in background con `ffmpeg` verso **MP4 H.264 + AAC, max 720p**. L'admin vede un badge "Transcoding XX%" sulla miniatura; quando finisce il file diventa riproducibile su tutti i browser.

> Le immagini e i PDF **non** vengono mai trasformati: sono salvati identici al file caricato.

La transcodifica richiede `ffmpeg`/`ffprobe` sul server — vedi [Prerequisiti](#prerequisiti-di-sistema). Se mancano, gli upload di video che richiedono ricodifica rispondono `503`.

---

## Limiti di testo

Ogni campo compilabile dall'admin ha un limite (vedi [`TEXT_LIMITS`](#limiti-testo-text_limits)). L'interfaccia mostra un contatore quando ci si avvicina al limite (rosso se superato) e blocca l'inserimento oltre il massimo. Lato server, una richiesta che sfora viene rifiutata con `400` e l'elenco dei campi fuori limite — nessun troncamento silenzioso.

Gli URL (`actionUrl`) accettano solo gli schemi `http`, `https`, `mailto`, `tel`. Schemi come `javascript:` vengono rifiutati.

---

## Sicurezza degli input

- **HTML delle card (`fullContent`)**: sanificato in scrittura con una whitelist (`p, br, strong, em, b, i, u, ul, ol, li, a, h1–h6, blockquote, span`). I link ricevono automaticamente `rel="noopener noreferrer" target="_blank"`. Script e attributi pericolosi vengono rimossi.
- **Welcome text del portale**: sanificato con whitelist ridotta (`b, i, strong, em, br, p, div, span`) — solo grassetto, corsivo e a-capo, nessun link. Si modifica con il mini-editor (pulsanti **B**/**I**) nelle impostazioni.
- **Colore tema (`themeColor`)**: accettato solo nel formato `#RRGGBB`.
- **Nomi file**: normalizzati (accenti rimossi, niente caratteri speciali, niente path traversal) e resi univoci con timestamp + stringa casuale.

---

## Struttura dei dati (`data/`)

Tutto lo stato applicativo vive nella cartella `data/` alla radice del progetto. Non contiene codice, solo dati caricati dagli utenti:

```
data/
├── cards.txt              ← tutte le card (JSON array)
├── portals.txt            ← configurazione del portale (JSON array)
├── fonts/                 ← font caricati (.woff2/.woff/.ttf/.otf)
├── uploads/               ← media caricati (immagini, video, pdf)
│   └── .tmp/              ← buffer temporaneo upload/transcoding (svuotabile)
└── transcode-jobs.json    ← stato della coda di transcodifica (creato all'occorrenza)
```

Copiare via questa cartella = backup completo. Sostituirla = ripristino completo. Nessun database, nessuna migrazione.

---

## Backup e ripristino

Disponibile dall'admin in **Settings → Backup & Restore**.

### Dall'interfaccia
- **⬇ Scarica backup ZIP** — scarica `interceptop-backup-<data>.zip` con card, configurazione, media e font (esclude i file temporanei).
- **⤴ Ripristina da ZIP…** — carica uno zip; dopo conferma sovrascrive lo stato attuale e ricarica la pagina. La cartella `data/` precedente viene conservata come `data.bak-<timestamp>/` come rete di sicurezza.

### Da riga di comando (Linux)

**Creare un backup** (il `cd data` è essenziale: mette i file alla radice dello zip):
```bash
cd /percorso/del/progetto/data
zip -r ~/backup-$(date +%Y%m%d-%H%M%S).zip cards.txt portals.txt uploads fonts -x "uploads/.tmp/*"
```

**Verificare la struttura** (devi vedere `cards.txt` e `portals.txt` in cima, NON una cartella `data/`):
```bash
unzip -l ~/backup-*.zip
```

Lo zip così prodotto è caricabile direttamente dal pulsante "Ripristina da ZIP…".

> **Struttura obbligatoria:** i file devono stare alla radice dello zip. Uno zip con tutto dentro una cartella `data/` verrà rifiutato con "cards.txt assente". Deve essere uno **ZIP**, non un `.tar`.

---

## Factory Preset (developer)

Stato "di fabbrica" ripristinabile con un click. Pensato per preparare preset standard (es. banner + icona + una card di redirect) da distribuire a più macchine MajorNet.

**Visibile nell'admin solo se** `FACTORY_RESET_ENABLED = true` in `lib/config.ts`. Gli endpoint API restano comunque attivi anche con il flag a `false`.

Il preset è un file fisso: **`factory/preset.zip`** alla radice del progetto (fuori da `data/`, quindi non viene toccato dai reset; fuori da `public/`, quindi non scaricabile via web).

### Dall'interfaccia (con flag attivo)
- **💾 Salva stato attuale come Factory Preset** — congela lo stato corrente in `factory/preset.zip`.
- **🏭 Factory Reset** — ripristina tutto al preset (disabilitato se il preset non esiste). La `data/` precedente resta come `data.bak-<timestamp>/`.

### Da riga di comando / API
```bash
# salva lo stato corrente come preset
curl -X POST http://localhost:3000/api/admin/factory-preset

# esegui il factory reset
curl -X POST http://localhost:3000/api/admin/factory-reset

# verifica se il preset esiste
curl http://localhost:3000/api/admin/factory-preset
```

### Creare il preset a mano (Linux)
```bash
cd /percorso/del/progetto/data
mkdir -p ../factory
zip -r ../factory/preset.zip cards.txt portals.txt uploads fonts -x "uploads/.tmp/*"
```

### Distribuzione su altre macchine
Copia `factory/preset.zip` sulle altre installazioni: l'admin (con flag attivo) vedrà il preset e potrà fare Factory Reset per allinearsi allo stato standard.

---

## Font

I font vanno collocati in `data/fonts/` nei formati `.woff2`, `.woff`, `.ttf`, `.otf`. Vengono inclusi automaticamente nei backup.

- L'elenco mostrato in admin esclude i file con `italic`/`bold` nel nome (si usa la variante "regular"; i pesi vengono gestiti dal browser).
- Si seleziona il font del portale dalle impostazioni; in alternativa si imposta `DEFAULT_FONT` in `lib/config.ts`.
- Il welcome text formattato (grassetto/corsivo) eredita il font selezionato.

---

## Prerequisiti di sistema

Sul server servono alcuni binari di sistema (richiamati direttamente, non via npm):

| Binario | A cosa serve | Se manca |
|---|---|---|
| `ffmpeg` | Transcodifica video non compatibili | Upload video che richiedono ricodifica → `503` |
| `ffprobe` | Riconoscimento codec video | Come sopra |
| `zip` | Creazione backup / preset | Pulsante "Scarica backup" e "Salva preset" → `503` |
| `unzip` | Ripristino backup / factory reset | Pulsanti di ripristino → `503` |

Verifica su Ubuntu:
```bash
which ffmpeg ffprobe zip unzip
```
Se mancano:
```bash
sudo apt install ffmpeg zip unzip
```

---

## Risoluzione problemi

**`npm run build` → "Module not found: file-type / sanitize-html"** anche se sono installati: cache Turbopack corrotta. Soluzione:
```bash
rm -rf .next && npm run build
```

**Un video caricato non parte nel browser**: probabilmente la transcodifica è fallita o `ffmpeg` non è installato. Controlla `which ffmpeg ffprobe` e i log del server.

**Il ripristino dice "cards.txt assente"**: lo zip ha una cartella `data/` di troppo al suo interno. Ricrealo facendo `cd data` PRIMA del comando `zip` (vedi [Backup](#backup-e-ripristino)).

**Recupero dopo un ripristino sbagliato**: lo stato precedente è in `data.bak-<timestamp>/`. Ferma il server, rinomina quella cartella in `data/` e riavvia.

**La sezione Factory Preset non compare**: è dietro il flag `FACTORY_RESET_ENABLED` in `lib/config.ts`. Mettilo a `true` e ricostruisci.
