diff --git a/scripts/calibrazione_pagina.py b/scripts/calibrazione_pagina.py
index 500ddb2..fbb7aee 100644
--- a/scripts/calibrazione_pagina.py
+++ b/scripts/calibrazione_pagina.py
@@ -1,80 +1,194 @@
-"""Pagina HTML della calibrazione a scacchiera (servita da camera_service su /calibrazione, #83).
+"""Pagina HTML dell'acquisizione camera (servita da camera_service su /calibrazione e /acquisizione, #83 #87).
-Cosa fa: una pagina sola, autonoma e offline (niente CDN, JS e CSS inline), in
-italiano. Parla con le rotte dello stesso servizio (/camera/sources,
-/camera/calibration, /camera/calibrate).
-#84: calibrazione densa, un solo dato dell'operatore (lato dello scacco in mm):
-scacchiera che riempie il campo, griglia trovata da sola, mappa di distorsione.
+Cosa fa: una pagina sola, autonoma e offline (niente CDN, niente font esterni,
+JS e CSS inline), in italiano, stile scuro TMF. Quattro sezioni: Sorgente,
+Anteprima (dal vivo, con nitidezza/saturazione/livello medio), Parametri camera
+(esposizione e guadagno), Calibrazione a scacchiera.
+Parla con le rotte dello stesso servizio: /camera/sources, /camera/anteprima,
+/camera/parametri, /camera/parametri/salva, /camera/calibration, /camera/calibrate.
+#84: calibrazione densa, un solo dato dell'operatore (lato dello scacco in mm).
+#87: anteprima, parametri camera, stile TMF.
Dipende da: niente. E' una stringa, cosi' il pacchetto compilato non ha file dati.
"""
-PAGINA = """
+PAGINA = r"""
-Calibrazione camera
+Acquisizione camera
-
Calibrazione camera con scacchiera
+
Acquisizione camera
-
-
-
-
+
+
+
+
+
Anteprima
+
+
+
+
+
+
+
+
+
Anteprima ferma. Premere «Avvia anteprima» o «Scatta».
+
+
+
+
+
+
+
+
Nitidezza
+
-
+
+
massimo visto: -
+
+
+
+
Saturati
+
-
+
pixel a 250 o più; da tenere sotto l'1 %
+
+
+
Livello medio
+
-
+
scala 0-255
+
+
+
Frame pieno
+
-
+
+
+
+
+
-
- Servono una scacchiera che riempie tutto il campo della camera, piana, ferma e ben illuminata, e il lato di uno scacco.
- La griglia si trova da sola; il risultato corregge anche la distorsione dell'ottica.
-
-
-
-
-
-
-
-
-
+
+
+
+
Sorgente
+
+
+
-
+
+
-
- Risultato del calcolo
-
-
-
-
- Da camera, salvando si acquisisce e si calcola di nuovo: tenere la scacchiera ferma.
-
-
-
+
+
+
Parametri camera
+
-
+
+
+
+
+
-
- Calibrazione salvata
-
-
-
-
+
+
Calibrazione
+
+
+
+
+ Servono una scacchiera che riempie tutto il campo della camera, piana, ferma e ben illuminata, e il lato di uno scacco.
+ La griglia si trova da sola; il risultato corregge anche la distorsione dell'ottica.
+ Durante il calcolo l'anteprima si sospende.
+
+
+
+
+ nessuna immagine scelta
+
+
+
+
+
+ Risultato del calcolo
+
+
+
+
+ Da camera, salvando si acquisisce e si calcola di nuovo: tenere la scacchiera ferma.
+
+
+
+
+
+ Calibrazione salvata
+
-
+
+
+
+
+
@@ -82,31 +196,437 @@ PAGINA = """
diff --git a/scripts/tests/test_pagina_acquisizione.py b/scripts/tests/test_pagina_acquisizione.py
new file mode 100644
index 0000000..052ce57
--- /dev/null
+++ b/scripts/tests/test_pagina_acquisizione.py
@@ -0,0 +1,54 @@
+"""Test della pagina Acquisizione camera (#87): contenuto statico di PAGINA, senza servizio."""
+
+import importlib.util
+import re
+from pathlib import Path
+
+_FILE = Path(__file__).resolve().parents[1] / "calibrazione_pagina.py"
+_spec = importlib.util.spec_from_file_location("calibrazione_pagina", _FILE)
+_mod = importlib.util.module_from_spec(_spec)
+_spec.loader.exec_module(_mod)
+PAGINA = _mod.PAGINA
+
+
+def test_titolo_e_stile_scuro():
+ assert "Acquisizione camera" in PAGINA
+ assert "#0f172a" in PAGINA and "#1e293b" in PAGINA and "#3b82f6" in PAGINA
+
+
+def test_rotte_usate():
+ for rotta in ("/camera/sources", "/camera/anteprima", "/camera/parametri",
+ "/camera/parametri/salva", "/camera/calibrate", "/camera/calibration"):
+ assert '"' + rotta in PAGINA, rotta
+
+
+def test_offline_e_senza_finestre_del_browser():
+ for vietato in ("alert(", "confirm(", "prompt(", "http://", "https://"):
+ assert vietato not in PAGINA, vietato
+
+
+def test_id_principali():
+ ids = set(re.findall(r'id="([^"]+)"', PAGINA))
+ attesi = {"sorgente", "avvia", "ferma", "scatta", "grezzo", "mirinoCasella", "immagine",
+ "nitidezza", "nitidezzaMax", "saturati", "media", "dimensioni",
+ "parametri", "salvaParametri", "quadrato", "acquisisci", "scegliFile", "file",
+ "carica", "salva", "elimina", "stato", "statoSorgente"}
+ assert attesi <= ids, attesi - ids
+ # ogni id usato dal JS con $("...") esiste nel markup
+ usati = set(re.findall(r'\$\("([^"]+)"\)', PAGINA))
+ assert usati <= ids, usati - ids
+
+
+def test_sezioni_e_testi_italiani():
+ for testo in ("Sorgente", "Anteprima", "Parametri camera", "Calibrazione",
+ "Salva come predefiniti", "Scegli immagine...", "Questa sorgente non ha parametri regolabili",
+ "Immagine grezza (senza correzione)"):
+ assert testo in PAGINA, testo
+ assert "it-IT" in PAGINA # numeri con la virgola
+ assert "Choose File" not in PAGINA
+
+
+def test_localstorage_protetto():
+ # ogni uso di localStorage sta dentro un try
+ for m in re.finditer(r"localStorage[.]", PAGINA):
+ assert "try {" in PAGINA[max(0, m.start() - 200):m.start()]