diff --git a/Dockerfile.frontend b/Dockerfile.frontend index f9e769d..c2e4a78 100644 --- a/Dockerfile.frontend +++ b/Dockerfile.frontend @@ -20,8 +20,14 @@ RUN uv sync --frozen --no-dev --extra client COPY src/frontend/flask_app/ ./flask_app/ # Build TailwindCSS (one-shot; no watcher in production image). +# +# Versione esatta, non `tailwindcss@3`. Con il vincolo aperto ogni ricostruzione +# dell'immagine poteva prendere una minor diversa e produrre un CSS diverso senza +# che nessuno l'avesse validato: per un sistema che produce evidenze per audit +# ISO 9001 / IATF 16949 è lo stesso problema che il punto 12 risolve per le +# librerie del browser. Aggiornarla è una decisione, non un effetto collaterale. WORKDIR /app/flask_app -RUN npm install tailwindcss@3 && \ +RUN npm install tailwindcss@3.4.19 && \ npx tailwindcss -i static/css/input.css -o static/css/tailwind.css --minify # Compile Flask-Babel translation catalogs. diff --git a/README.md b/README.md index e105d9f..87ea364 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ Sistema di gestione task per misurazioni con calibro manuale. Soluzione tablet-first, multi-ruolo, con statistiche SPC (Statistical Process Control) integrate, identità per-stazione e rate limiting per-tablet. -> **Versione corrente:** V2.0.0 (in sviluppo) — branch default `V2.0.0`. +> **Versione corrente:** V3.0.0 (in sviluppo) — branch `V3.0.0`. > Per stato dettagliato e prossimi passi vedi [`docs/architecture/STATO_PROGETTO.md`](docs/architecture/STATO_PROGETTO.md) e [`docs/architecture/ROADMAP.md`](docs/architecture/ROADMAP.md). +> V3.0.0 lavora sui quindici punti di `TieMeasureFlow_modifiche_2026-07-28.md`: vedi [Novità di V3.0.0](#novità-di-v300). --- @@ -25,6 +26,32 @@ Caratteristiche principali: --- +## Novità di V3.0.0 + +V3.0.0 lavora sui quindici punti raccolti in `TieMeasureFlow_modifiche_2026-07-28.md`, +dal sopralluogo del 28/07. Quanto segue è **fatto e in esercizio**. + +| # | Punto | Cosa cambia | +|---|---|---| +| 1 · 6 | **Produzione con una vita propria** | `production_runs` e `production_events` sul server: avvio, fermo linea, ripresa e fine produzione esistono come stato, non come pulsanti. La fine produzione emette il file di statistica dell'intera produzione | +| 2 | **Tipo di task dichiarato** | `task_type` (`note`, `measure`, `drawing`, `xf_compare`, `camera_measure`) al posto di «ha subtask, quindi è una misura»: un task di misura senza quote non si comporta più come una nota | +| 3 | **Ciclo di misura** | L'intervallo della ricetta vive sul server, sopravvive al cambio pagina, conta anche il ritardo (in rosso, oltre lo zero) e riporta l'operatore alla misura. Cicalino via WebAudio | +| 5 | **Fuori tolleranza vincolante** | Una quota fuori tolleranza blocca l'avanzamento finché il capoturno non autorizza — o finché quella stessa quota non viene rimisurata dentro i limiti. L'autorizzazione resta scritta sulla misura e nel CSV | +| 7 | **Stazioni** | Ricette per stazione, cambio stazione da URL per il collaudo, reset per riga | +| 8 | **Tracciabilità obbligatoria** | `requires_lot` / `requires_serial` per ricetta, verificati sul server: nessuna porta d'ingresso li aggira, barcode compreso | +| 9 | **Blocco inserimento manuale** | `allow_manual_input`: con la ricetta a solo calibro il tastierino non viene disegnato affatto, e il server rifiuta comunque un valore digitato a mano | +| 10 | **Sequenza per l'operatore** | La ricetta si apre sul primo task, non su un elenco. La lista scende a secondo livello e dice quali task sono rimasti incompiuti (`2/3`). «Fine ciclo misura» è visibile da subito e spento finché mancano quote | +| 11 | **Descrizioni formattate** | `**grassetto**` e a capo nelle descrizioni dei task, con marcatura e non HTML: la sanificazione è per costruzione | +| 12 | **Funzionamento senza internet** | Tutte le librerie e i font nell'installazione, versioni congelate e verificate per impronta, Content-Security-Policy a sola origine locale | +| 14 | **Layout stabile** | Una cornice sola per tutte le viste, allineata alla navbar; spazio della barra di scorrimento sempre riservato; schermata di misura su `100dvh`. Vedi [`docs/architecture/LAYOUT.md`](docs/architecture/LAYOUT.md) | +| 15 | **Statistica separata** | Si registra sempre, si consulta a parte: nessun percorso dell'operatore porta alla statistica (verificato da test) | + +**Aperto**: il punto 4 (numero di tentativi prima del capoturno) attende le risposte +del cliente su modalità di autorizzazione e numero di tentativi; il punto 13 +(generazione task dalla scheda tecnica con l'AI) è fuori offerta. + +--- + ## Architettura ``` @@ -67,13 +94,22 @@ Il frontend Flask non espone mai le credenziali al browser: ogni chiamata al bac | Flask | 3.x | Framework web server-side | | gunicorn | 21+ | WSGI server (5 workers × 4 thread gthread) | | Jinja2 | incluso in Flask | Template engine | -| Alpine.js | 3.x (CDN) | Reattività leggera lato client | -| TailwindCSS | 3.x | CSS utility-first | -| Plotly.js | CDN | Grafici SPC interattivi | -| Fabric.js | 5.3.1 (CDN) | Editor annotazioni disegni tecnici | -| html5-qrcode | CDN | Scanner barcode/QR camera | +| Alpine.js | 3.15.12 (locale) | Reattività leggera lato client | +| TailwindCSS | 3.4.19 (build) | CSS utility-first, compilato nell'immagine | +| Plotly.js | 2.32.0 (locale) | Grafici SPC interattivi | +| PDF.js | 3.11.174 (locale) | Visualizzazione disegni PDF, worker incluso | +| Fabric.js | 5.3.1 (locale) | Editor annotazioni disegni tecnici | +| Inter + JetBrains Mono | woff2 locali | Font UI e numeri | | Flask-Babel | ultima stabile | i18n IT/EN | +**Nessuna libreria arriva dalla rete.** Tutte stanno in +[`src/frontend/flask_app/static/vendor/`](src/frontend/flask_app/static/vendor/VERSIONS.md) +con versione nel nome e impronta SHA-256 verificata da un test: l'installazione a +Tràfilo è su rete di produzione isolata, dove una pagina che aspetta un CDN è una +pagina bianca. Una Content-Security-Policy a sola origine locale (`app.py`) fa sì che +un tag verso l'esterno aggiunto in futuro venga rifiutato alla scrivania, non in +reparto. + ### Tooling | Componente | Ruolo | @@ -211,7 +247,8 @@ I ruoli sono combinabili (array JSON per utente). Il flag `is_admin` è separato |---|---| | **Maker** | Crea e gestisce ricette di misurazione: caricamento disegni (PDF/immagini), annotazioni Fabric.js, definizione task/subtask, configurazione tolleranze, versioning copy-on-write | | **MeasurementTec** | Esegue misurazioni: scansione barcode per selezione ricetta, interfaccia task-driven, input da calibro USB HID o numpad touch, validazione real-time pass/warning/fail. Vede solo le ricette assegnate alla propria stazione (`STATION_CODE`) | -| **Metrologist** | Analisi qualità: dashboard SPC (X-bar, R, Cp, Cpk, Pp, Ppk), filtri multi-dimensionali, export report PDF, analisi capability e control chart | +| **Supervisor** (capoturno) | Autorizza ciò che l'operatore non può decidere da solo: una quota fuori tolleranza che deve restare, il fermo linea, la fine produzione. L'autorizzazione resta scritta sulla misura (`supervisor_id`, `authorised_at`) e finisce nel file di statistica | +| **Metrologist** | Analisi qualità: dashboard SPC (X-bar, R, Cp, Cpk, Pp, Ppk), filtri multi-dimensionali, export report PDF, analisi capability e control chart. **L'operatore non raggiunge la statistica da nessun percorso** | | **Admin** (flag) | Gestione sistema: CRUD utenti, cambio password, attivazione/disattivazione account, **CRUD stazioni e assegnazioni ricette** | --- @@ -229,6 +266,7 @@ TieMeasureFlow/ ├── docker-compose.yml # Produzione (Traefik, SSL) ├── nginx/ # Config Nginx (dev) ├── uploads/ # Volume Docker file caricati +├── scripts/ # Script del progetto (seed ricette di collaudo) ├── docs/ # Documentazione (vedi indice docs/README.md) └── src/ ├── backend/ # FastAPI Backend @@ -245,11 +283,14 @@ TieMeasureFlow/ │ │ │ # RecipeTask, RecipeSubtask, Measurement, │ │ │ # AccessLog, SystemSetting, │ │ │ # RecipeVersionAudit, Station, - │ │ │ # StationRecipeAssignment + │ │ │ # StationRecipeAssignment, + │ │ │ # ProductionRun, ProductionEvent │ │ └── api/ # Pydantic v2 schemas request/response │ ├── services/ # recipe_service, measurement_service, │ │ # spc_service, report_service, - │ │ # auth_service, station_service + │ │ # auth_service, station_service, + │ │ # production_service, + │ │ # production_export_service │ ├── migrations/ # Alembic (alembic.ini + env.py) │ ├── templates/ # Pagina setup (Jinja2) │ └── tests/ # pytest + httpx + aiosqlite @@ -262,9 +303,13 @@ TieMeasureFlow/ ├── services/ # APIClient (proxy verso FastAPI con XFF) ├── templates/ # Jinja2 + Alpine.js ├── static/ - │ ├── css/ # TailwindCSS compilato - │ └── js/ # numpad, caliper, barcode, csv-export, - │ # spc-charts, annotation-editor/viewer + │ ├── css/ # TailwindCSS compilato + themes.css + │ │ # (cornice pagina, temi, scrollbar) + │ ├── js/ # numpad, caliper, barcode, csv-export, + │ │ # spc-charts, annotation-editor/viewer, + │ │ # production-clock, rich-text + │ └── vendor/ # Alpine, Plotly, PDF.js (+worker), Fabric, + │ # font woff2 — vedi VERSIONS.md ├── translations/ # Flask-Babel .po/.mo IT/EN └── tests/ ``` @@ -341,7 +386,16 @@ uv run pytest --cov src/backend uv run pytest src/frontend/flask_app/tests/ ``` -Stato corrente: **183 pass, 0 fail** (vedi `docs/architecture/STATO_PROGETTO.md`). +Stato corrente su `V3.0.0`: **360 pass, 0 fail** (212 backend + 148 frontend). + +Alcuni test frontend non renderizzano niente e leggono i sorgenti, perché guardano +proprietà che sopravvivono solo se qualcuno le controlla: + +| File | Cosa impedisce | +|---|---| +| `test_offline.py` | Una libreria caricata dalla rete, un worker PDF.js lasciato sul CDN, una libreria sostituita senza aggiornare l'impronta | +| `test_layout_shell.py` | Una vista che torna a dichiararsi la propria larghezza | +| `test_template_js_syntax.py` | Una traduzione con l'apostrofo dentro una stringa JS a virgolette singole, che spegne Alpine su tutta la pagina | --- @@ -377,6 +431,7 @@ Indice completo: [`docs/README.md`](docs/README.md). | Documento | Contenuto | |---|---| +| [`TieMeasureFlow_modifiche_2026-07-28.md`](TieMeasureFlow_modifiche_2026-07-28.md) | I quindici punti del 28/07: cosa deve cambiare, dove intervenire, decisioni in attesa del cliente (D-1…D-9) | | [`docs/architecture/STATO_PROGETTO.md`](docs/architecture/STATO_PROGETTO.md) | Snapshot V2.0.0: cosa funziona oggi, test status, decisioni architetturali | | [`docs/architecture/ROADMAP.md`](docs/architecture/ROADMAP.md) | Cosa resta da fare (Fasi 2-7 rev04, decisioni cliente aperte, stime) | @@ -388,6 +443,8 @@ Indice completo: [`docs/README.md`](docs/README.md). | [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) | Guida deployment VPS: Docker, Traefik, SSL, DNS, firewall | | [`docs/USER_GUIDE.md`](docs/USER_GUIDE.md) | Manuale utente per ruolo (Maker, MeasurementTec, Metrologist) | | [`docs/I18N_SETUP.md`](docs/I18N_SETUP.md) | Setup e workflow traduzioni (Flask-Babel + Alpine.js) | +| [`docs/architecture/LAYOUT.md`](docs/architecture/LAYOUT.md) | La cornice delle pagine: perché il layout si spostava e la regola che lo tiene fermo | +| [`src/frontend/flask_app/static/vendor/VERSIONS.md`](src/frontend/flask_app/static/vendor/VERSIONS.md) | Librerie di terze parti in locale: versioni, impronte, come aggiornarle | ### Piani dettagliati diff --git a/src/frontend/flask_app/app.py b/src/frontend/flask_app/app.py index 0a5d5de..6d78580 100644 --- a/src/frontend/flask_app/app.py +++ b/src/frontend/flask_app/app.py @@ -17,6 +17,28 @@ from config import Config # line break. DOTALL so a phrase that wraps onto the next line still closes. _BOLD_RX = re.compile(r"\*\*(.+?)\*\*", re.DOTALL) +# Punto 12. Nothing loads from outside: every library ships with the install +# (static/vendor/), because the shop floor network has no way out and a page that +# waits for a CDN there is a white screen, not a slow one. +# +# unsafe-inline the templates carry their Alpine components inline +# unsafe-eval Alpine 3 and Plotly both compile expressions at runtime +# blob: PDF.js runs its worker from a blob, Fabric exports canvases +# data: thumbnails and canvas exports are inlined +CSP = ( + "default-src 'self'; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " + "style-src 'self' 'unsafe-inline'; " + "font-src 'self'; " + "img-src 'self' data: blob:; " + "connect-src 'self'; " + "worker-src 'self' blob:; " + "object-src 'none'; " + "base-uri 'self'; " + "form-action 'self'; " + "frame-ancestors 'none'" +) + def get_locale(): """Get user's preferred language from session or Accept-Language header.""" @@ -115,6 +137,25 @@ def create_app() -> Flask: ) return Markup(rv) + @app.after_request + def security_headers(response): + """Same-origin only, on the pages the browser actually loads. + + The policy existed on the backend, which serves the API; the HTML comes + from here and carried no policy at all. So the rule that says «nothing + from outside» was written where it could not be broken and absent where + it could. + + It is also the guard that makes point 12 stay done: a CDN tag added to a + template months from now is refused by the browser here, at a desk with a + network, instead of on a shop floor that has none. + """ + response.headers.setdefault("Content-Security-Policy", CSP) + response.headers.setdefault("X-Content-Type-Options", "nosniff") + response.headers.setdefault("X-Frame-Options", "DENY") + response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin") + return response + @app.context_processor def inject_globals(): """Inject global variables into all templates.""" diff --git a/src/frontend/flask_app/static/vendor/VERSIONS.md b/src/frontend/flask_app/static/vendor/VERSIONS.md index 3cea9dc..8c1689e 100644 --- a/src/frontend/flask_app/static/vendor/VERSIONS.md +++ b/src/frontend/flask_app/static/vendor/VERSIONS.md @@ -64,7 +64,49 @@ db5ff4db83e580426280e9337a58dc57d3a83784a1b03ad80914651594441d52 fonts/jetbrain ``` Verifica: `sha256sum -c` da questa cartella, oppure -`sha256sum *.js fonts/*.woff2`. +`sha256sum *.js fonts/*.woff2`. Il confronto è anche un test +(`tests/test_offline.py::test_the_shipped_libraries_are_the_ones_that_were_checked`): +una libreria sostituita sul posto tiene lo stesso nome e la stessa riga in tabella, +e l'impronta è l'unica parte che se ne accorge. + +## La regola che tiene il punto chiuso + +Le copie locali risolvono il problema una volta; quello che lo tiene risolto è la +**Content-Security-Policy** servita dal client Flask (`app.py`, `CSP`), che ammette +solo l'origine locale. Prima esisteva solo sul backend — cioè sulle risposte API, +non sulle pagine che il browser disegna: la regola stava scritta dove non poteva +essere infranta e mancava dove poteva. + +Con la policy in vigore, un `