docs: porta stato e roadmap ai fatti del 28/07

Erano fermi allo snapshot V2.0.0 di fine aprile: parlavano di Fasi rev04 da
iniziare, di quattro test rotti e di uno stack che scaricava le librerie da CDN.
Niente di tutto questo è più vero, e un documento di stato sbagliato è peggio di
un documento di stato assente — qualcuno ci si fida.

STATO_PROGETTO: i quindici punti con il loro stato e dove vivono nel codice, cosa
è entrato in V3.0.0 raggruppato per tema, le dieci migrazioni, la suite a 360
pass. In fondo la sezione che conta di più, «cosa non è stato provato»: tredici
punti sono in esercizio e nessuno li ha percorsi su un tablet, il punto 14 non è
mai stato riprodotto su un dispositivo, il punto 12 non è mai stato provato a rete
staccata. Serve a non confondere «i test passano» con «funziona in reparto».

ROADMAP: riscritta intorno a quello che resta e in che ordine — il collaudo prima
di tutto, perché è l'unico lavoro che non aspetta risposte da nessuno, con la
tabella di cosa guardare e come si vede che è giusto. Poi il punto 4 (due o tre
giorni, a decisioni chiuse), l'innesto GAIA e l'installazione di settembre.

Le decisioni aperte sono ora le D-1…D-9 del documento del 28/07, non più le
D-0.x di aprile: la corrispondenza è scritta, così chi torna sui vecchi documenti
non si perde. Stessa cosa per le sette Fasi rev04, con dove è finita ciascuna:
due assorbite, una sostituita, una ridimensionata dal fatto che una rete isolata
rende l'aggiornamento automatico privo di senso.

Lo snapshot V2.0.0 è conservato in docs/archive/ invece di essere sovrascritto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-28 22:03:40 +00:00
parent c4a429d952
commit 2101b9b2c9
4 changed files with 437 additions and 188 deletions
+157 -110
View File
@@ -1,145 +1,192 @@
# Stato Progetto TieMeasureFlow — V2.0.0
# Stato Progetto TieMeasureFlow — V3.0.0
> Snapshot al 2026-04-27. Aggiornare ad ogni milestone.
> Snapshot al 2026-07-28. Aggiornare ad ogni milestone.
> Lo snapshot V2.0.0 è in [`../archive/STATO_PROGETTO_V2.0.0_2026-04-27.md`](../archive/STATO_PROGETTO_V2.0.0_2026-04-27.md).
## Versione corrente
**V2.0.0** (in sviluppo, branch `V2.0.0` come default su `git.tielogic.xyz`).
**V3.0.0** (in sviluppo, branch `V3.0.0`). Versione precedente: `V2.0.0`.
Versione precedente di produzione: `V1.0.7`.
In esercizio su `tieflow.tielogic.xyz` (Docker Compose + Traefik + Let's Encrypt),
schema alla migrazione `010_meas_authorisation`.
## Sintesi esecutiva
Il sistema base (V1.0.7) è completo e collaudato: ricette, task, misurazioni, SPC, report PDF, gestione utenti, dashboard metrologist. La V2.0.0 in corso aggiunge il primo blocco della migrazione **rev04** (stazioni per-tablet) e ristruttura l'intero monorepo secondo lo standard `python-project-spec-design.md` (uv + `src/backend/` + `src/frontend/flask_app/`).
V2.0.0 aveva chiuso stazioni per-tablet, ristrutturazione monorepo con `uv` e la
tenuta multi-tablet. **V3.0.0 nasce dal sopralluogo del 28/07 e lavora sui quindici
punti** raccolti in [`TieMeasureFlow_modifiche_2026-07-28.md`](../../TieMeasureFlow_modifiche_2026-07-28.md).
## Cosa funziona oggi (V2.0.0 — branch corrente)
Il filo che li tiene insieme: fino a V2.0.0 il sistema *registrava* misure ma non
*governava* la produzione. Le regole vivevano nell'interfaccia — una modale che si
poteva chiudere, un timer che moriva al cambio pagina, un tipo di task dedotto dalla
presenza di quote. V3.0.0 le sposta sul server, dove non si aggirano.
### Funzionalità ereditate da V1.0.7
- Autenticazione username/password + API key per-utente, ruoli combinabili (Maker, MeasurementTec, Metrologist) + flag `is_admin`.
- Recipe versioning copy-on-write: una nuova versione si crea solo se la corrente ha già measurements; altrimenti update in-place.
- Editor ricette (Maker) con annotation editor Fabric.js (~1200 LOC, collaudato su tablet).
- Workflow operatore tablet: select_recipe → task_list → task_execute → task_complete, con barcode scanner e numpad touch (input USB calibro con burst detection).
- Calcolo pass/fail con limiti UTL/UWL/LWL/LTL.
- Dashboard SPC: capability (Cp/Cpk/Pp/Ppk), control chart (UCL/LCL = mean ± 3σ), istogramma con curva normale, calcoli puro stdlib (no numpy).
- Report PDF (WeasyPrint + Kaleido SVG).
- Setup page protetta da `SETUP_PASSWORD` per inizializzazione DB e seed.
- i18n IT/EN (Flask-Babel + Alpine.js JSON).
- Tema light/dark via `Alpine.store('theme')` + localStorage.
Tredici punti su quindici sono fatti e in esercizio. Uno è fermo sulle risposte del
cliente, uno è fuori offerta.
### Aggiunte V2.0.0 (rev04 Fase 1 — Stazioni per-tablet)
- Tabelle `stations` + `station_recipe_assignments` (Alembic migration `002_add_stations.py`).
- Modelli ORM: `Station`, `StationRecipeAssignment` con vincolo unique `(station_id, recipe_id)`.
- Schemas Pydantic: `StationCreate/Update/Response`, `StationRecipeAssignmentCreate/Response`, `RecipeSummary`.
- Service `station_service` (CRUD + assegnazioni + cascade delete).
- Router `/api/stations` con CRUD admin + endpoint operatore `GET /api/stations/by-code/{code}/recipes`.
- Seed automatico `ST-DEFAULT` con tutte le ricette esistenti (idempotente).
- Variabile env client `STATION_CODE` letta da `Config`, helper `APIClient.get_station_recipes()`.
- Filtro `select_recipe`: il client mostra solo le ricette assegnate alla propria stazione, errore se `STATION_CODE` non configurato.
- **GUI admin completa** in `/admin/stations`: tabella con search, modal create/edit, modal gestione assegnazioni ricette, conferma eliminazione, link in navbar (desktop + mobile).
- 47 nuovi test (32 server + 15 client) tutti pass.
## I quindici punti
### Aggiunte V2.0.0 (performance + multi-utente)
- Gunicorn 5 workers × 4 thread (gthread) — capacità ~20 richieste concorrenti Flask, regge 20+ tablet.
- Uvicorn 4 workers + `--proxy-headers --forwarded-allow-ips='*'`.
- Rate limit middleware: identificazione IP reale via `X-Forwarded-For``X-Real-IP``request.client.host`.
- Rate limit general 100 → 300 req/min/IP (per-tablet ora, non più condiviso).
- Flask `ProxyFix(x_for=1, x_proto=1, x_host=1)` per IP reale dietro Nginx.
- `APIClient` propaga `X-Forwarded-For` + `X-Real-IP` (sia JSON che multipart).
- 12 test aggiuntivi (7 server + 5 client).
| # | Punto | Stato | Dove vive |
|---|---|---|---|
| 1 | Memoria della produzione in corso | ✅ | `production_runs`, `production_service` |
| 2 | Tipo di task esplicito | ✅ | `RecipeTask.task_type`, migrazione 007 |
| 3 | Loop di misura e ripetizione | ✅ | `production-clock.js`, `production_service`, migrazione 008 |
| 4 | Limite di tentativi prima del capoturno | ⛔ **Fermo su D-6, D-7, D-9** | — |
| 5 | Avanzamento solo se in tolleranza | ✅ | `measurement_service.pending_authorisation`, migrazione 010 |
| 6 | Fermo linea e Fine produzione con effetto | ✅ | `production_service`, `production_export_service` |
| 7 | Gestione delle stazioni | ✅ | `station_service`, `/admin/stations` |
| 8 | Tracciabilità obbligatoria | ✅ | `Recipe.requires_lot/serial`, migrazione 009 |
| 9 | Blocco dell'inserimento manuale | ✅ | `Recipe.allow_manual_input`, `numpad.js` |
| 10 | Interfaccia operatore: sequenza e conferme | ✅ | `/measure/start`, `task-progress`, `task_list`, `task_execute` |
| 11 | Formattazione delle descrizioni | ✅ | filtro `rich_text`, `rich-text.js` |
| 12 | Funzionamento senza internet | ✅ | `static/vendor/`, CSP in `app.py` |
| 13 | Generazione task dalla scheda tecnica con l'AI | — **Fuori offerta** | — |
| 14 | Stabilità del layout | ✅ | `.tmf-page` in `themes.css`, [`LAYOUT.md`](LAYOUT.md) |
| 15 | Statistica separata dalla registrazione | ✅ | `@role_required("Metrologist")`, test dedicato |
### Aggiunte V2.0.0 (struttura monorepo)
- `pyproject.toml` unico con extra `server`/`client`/`dev`. Niente più `requirements.txt`.
- `uv.lock` (77 pacchetti) + `.python-version` (3.11) committati per build riproducibili.
- Layout `src/backend/` + `src/frontend/flask_app/` (vedi sotto).
- `Dockerfile` (root) + `Dockerfile.frontend` riscritti con `uv sync --frozen --no-dev --extra server|client`.
- `docker-compose.{dev,}.yml` con build context `.`.
- Alembic env.py aggiunge project root a `sys.path`; `script_location = %(here)s` resta valido.
- `.dockerignore` aggiornato.
## Cosa è entrato in V3.0.0
### Hardening post-restructure (smoke test 2026-04-26)
### Produzione come stato, non come pulsante (punti 1, 6)
Sequenza di smoke test in locale (uvicorn + gunicorn + MySQL Docker) ha fatto emergere quattro regressioni che sarebbero rimaste invisibili al test suite:
- Tabelle `production_runs` e `production_events`; una produzione si apre su una
stazione, accumula eventi (`start`, `cycle_completed`, `task_measured`,
`remeasure`, `line_stop`, `resume`, `close`) e si chiude.
- Fermo linea, ripresa e fine produzione richiedono le credenziali del capoturno e
**cambiano lo stato**: prima erano modali che si chiudevano.
- La fine produzione emette il file di statistica dell'intera produzione
(`production_export_service`), CSV con i delimitatori configurati, marcando le
misure come esportate. Il punto d'innesto verso GAIA è lì accanto, in attesa di
D-1 e D-2.
- **`src/backend/config.py`**: `env_file` era cwd-relative (`../../.env`). Rotto fuori da `src/backend/`. Risolto con percorso assoluto `Path(__file__).resolve().parents[2] / ".env"`.
- **`src/backend/models/orm/__init__.py`**: `Station` e `StationRecipeAssignment` non erano esportati, quindi `Base.metadata.create_all` non creava le tabelle stations. Aggiunti agli import.
- **`.env.example`**: `UPLOAD_DIR=server/uploads` era residuo della vecchia struttura → file landavano fuori dall'albero di progetto. Aggiornato a `UPLOAD_DIR=uploads`.
- **Apostrofi italiani in template Alpine** (`l'utente`, `nell'eliminazione`, `nell'assegnazione`): chiudevano prematuramente JS string literals dentro `x-text` e blocchi `<script>`. Riscritti con delimitatori `&quot;...&quot;` o riformulazione testuale.
### Il ciclo di misura (punto 3)
Inoltre **UX rework** della modale assegnazione ricette su `/admin/stations`: dropdown sostituita da layout a 2 colonne (disponibili / assegnate) con bottone inline `+ Assegna`, search filter, empty state esplicativo (mostrava silenziosamente lista vuota se tutte le ricette erano già assegnate).
- L'intervallo della ricetta è calcolato dal server (`seconds_to_next_measurement`,
`overdue`, `server_time`): sopravvive al cambio task, che è un ricaricamento di
pagina, e non si azzera cambiando schermata.
- Il conteggio **prosegue oltre lo zero**: il ritardo si vede, in rosso.
- A scadenza l'operatore viene riportato al primo task di misura della ricetta, con
cinque secondi di preavviso; chi sta già misurando viene lasciato finire.
- Cicalino via WebAudio, nessun file audio da caricare.
- `remeasure` distingue una seconda lettura dello stesso pezzo da un ciclo nuovo:
girare il pezzo e rimisurare non deve far ripartire l'intervallo.
Test guard aggiunto: `test_template_js_syntax.py` valida ogni inline `<script>` E ogni espressione Alpine (`x-*`, `@*`, `:*`) della pagina con `node --check`. Cattura automaticamente il bug-class apostrofo. Skip se Node non è installato.
### Le regole della ricetta (punti 8, 9)
## Layout repository (V2.0.0)
- `requires_lot`, `requires_serial`, `allow_manual_input` per ricetta, verificati in
`measurement_service`: **ogni porta d'ingresso passa di lì**, barcode compreso.
- Con `allow_manual_input` a falso il tastierino non viene disegnato — non nascosto:
il markup non esiste — e il server rifiuta comunque un valore digitato a mano.
- Il rilevamento del calibro USB è stato reso più tollerante (Enter veloce), perché
con la regola stretta una lettura corta come `9.5` veniva scambiata per digitazione.
```
TieMeasureFlow/
├── pyproject.toml + uv.lock + .python-version
├── Dockerfile (backend) + Dockerfile.frontend
├── docker-compose.dev.yml + docker-compose.yml
├── nginx/
├── uploads/ # volume Docker
├── docs/ # raggruppata e indicizzata
│ ├── README.md (indice)
│ ├── API.md / DEPLOYMENT.md / USER_GUIDE.md / I18N_SETUP.md
│ ├── architecture/ # questo file + ROADMAP.md
│ ├── archive/ # piani storici
│ ├── specs/ # spec esterne (.docx)
│ └── superpowers/plans/ # piani TDD dettagliati
└── src/
├── backend/
│ ├── main.py / config.py / database.py
│ ├── api/{routers,middleware}/
│ ├── models/{orm,api}/
│ ├── services/
│ ├── migrations/
│ ├── templates/
│ └── tests/
└── frontend/
└── flask_app/
├── app.py / config.py / compile_translations.py
├── blueprints/ (auth, maker, measure, statistics, admin)
├── services/ (api_client.py)
├── templates/ + static/ + translations/
└── tests/
```
### Il fuori tolleranza (punto 5)
## Smoke test status
- Una quota fuori tolleranza e non autorizzata **blocca** il salvataggio della quota
successiva e la chiusura del ciclo (409).
- Conta l'**ultima** lettura di ogni quota: rimisurare la stessa quota resta
possibile — il calibro scivola, il pezzo si riposiziona — e una lettura dentro i
limiti scioglie il blocco senza chiamare nessuno.
- L'autorizzazione del capoturno resta scritta sulla misura (`supervisor_id`,
`authorised_at`) e finisce in due colonne del CSV di produzione: il file mostra il
guasto **e** la decisione.
- Quanti tentativi siano ammessi è il punto 4, non questo.
Validazione end-to-end in locale (2026-04-26):
### L'interfaccia dell'operatore (punti 10, 11, 14, 15)
- ✅ MySQL container Docker up, schema creato, alembic stamp head OK
- ✅ uvicorn `--reload` su :8000, `/api/health` risponde
- ✅ Seed `/api/setup/seed` con `SETUP_PASSWORD=adriano77` → admin + 4 utenti demo + DEMO-001 + ST-DEFAULT con assegnazione automatica
- ✅ Login `admin/admin123` via web, sessione persistente
-`/admin/stations`: tabella, modal create/edit, modal gestione assegnazioni a 2 colonne con search, eliminazione con cascade
- `/admin/users`, `/maker/recipes`, `/measure/select` (filtrato per stazione), `/statistics/dashboard`
- ✅ Workflow MeasurementTec end-to-end: select_recipe → task_list → task_execute → task_complete (riepilogo con misure)
- ✅ Hot reload Flask + uvicorn `--reload` + Tailwind watch attivi durante lo sviluppo
- La ricetta si apre sul primo task (`/measure/start/<id>`), non su un elenco. La
lista scende a secondo livello e mostra quali task sono rimasti incompiuti
(`GET /api/measurements/task-progress`, contato per quota e non per tentativo).
- «Fine ciclo misura» è visibile da subito e spento finché mancano quote, con
scritto quante ne mancano.
- Descrizioni con `**grassetto**` e a capo: marcatura, non HTML, così la
sanificazione è per costruzione.
- Una cornice sola per tutte le viste, allineata alla navbar — vedi [`LAYOUT.md`](LAYOUT.md).
- Nessun percorso dell'operatore porta alla statistica, ed è un test.
### Fuori dalla rete (punto 12)
- Alpine, Plotly, PDF.js (+ worker), Fabric e i font sono nell'installazione, con
versione nel nome e impronta SHA-256 verificata da un test.
- Content-Security-Policy a sola origine locale servita **dal client Flask**: prima
esisteva solo sul backend, cioè sulle risposte API e non sulle pagine.
- Tailwind fissato a `3.4.19` nel `Dockerfile.frontend`.
## Migrazioni
| # | Contenuto |
|---|---|
| 001004 | image_path, stazioni, intervallo di misura + auto-logout, `input_duration_ms` |
| 005006 | `production_runs`, misure legate alla produzione |
| 007 | `task_type` dichiarato |
| 008 | tipi di evento del ciclo + `task_id` sull'evento |
| 009 | `requires_lot`, `requires_serial`, `allow_manual_input` |
| 010 | `supervisor_id`, `authorised_at` sulla misura |
Le 008 e 010 usano il *batch mode* per compatibilità SQLite (i test) e sono state
verificate anche in resa MySQL (`alembic upgrade X:Y --sql`) prima del deploy.
La 009 imposta `allow_manual_input = 1` sulle ricette esistenti: conserva il
comportamento in essere invece di irrigidire di colpo ricette già in uso. È una
scelta, e va confermata dal cliente ricetta per ricetta.
## Test status
| Backend (`src/backend/tests/`) | 127 | 3 | Fail pre-esistenti: `test_recipes` (2) + `test_tasks` (1). Nessuno introdotto dalla V2.0.0. |
| Frontend (`src/frontend/flask_app/tests/`) | 46 | 1 | +2 test post-restructure (`test_template_js_syntax.py`). Fail pre-esistente: `test_save_measurement_proxy`. |
| **Totale** | **173** | **4** | Tutti i fallimenti tracciati come tech debt da risolvere. |
## Stack confermato
| | Test | Fail |
|---|---|---|
| Backend (`src/backend/tests/`) | 212 | 0 |
| Frontend (`src/frontend/flask_app/tests/`) | 148 | 0 |
| **Totale** | **360** | **0** |
- **Backend:** FastAPI + SQLAlchemy 2.0 async + MySQL 8 + Alembic + Pydantic v2 + WeasyPrint + Plotly/Kaleido.
- **Frontend:** Flask + Jinja2 + Alpine.js + TailwindCSS + Fabric.js 5.3.1 + html5-qrcode + Plotly.js + Flask-Babel.
- **Deploy:** Docker Compose. Dev = Nginx; Prod = Traefik + Let's Encrypt SSL.
- **Tooling:** uv (package mgmt), pytest + pytest-asyncio + httpx + aiosqlite (test).
I quattro fallimenti pre-esistenti tracciati nello snapshot V2.0.0 non ci sono più.
Tre file di test 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 |
## Cosa non è stato provato
Va detto perché non si confonda «i test passano» con «funziona in reparto».
- **Il collaudo sul campo non è mai stato percorso.** Le ricette `COLLAUDO-A` e
`COLLAUDO-B` sono state seminate sul sistema in esercizio (`scripts/seed_collaudo.py`)
insieme a un utente `capoturno`, ma nessuno ha ancora guidato il flusso su un
tablet vero.
- **Il punto 14 non è stato riprodotto su un dispositivo**: è dimostrato che il
layout *poteva* muoversi per quattro motivi e che ora non può più, non che
l'operatore vedesse esattamente quelli.
- **Il punto 12 non è stato provato a rete staccata.** È dimostrato che nessuna
risorsa esterna viene richiesta. Il giro con la rete staccata va fatto, ed è ora il
momento giusto perché la CSP è appena entrata in vigore.
- **Il cicalino** è il suono del browser. Se serva una colonnina luminosa è D-5.
## Stack
- **Backend:** FastAPI + SQLAlchemy 2.0 async + MySQL 8 + Alembic + Pydantic v2 +
WeasyPrint + Plotly/Kaleido. 12 router.
- **Frontend:** Flask + Jinja2 + Alpine.js 3.15.12 + TailwindCSS 3.4.19 +
Fabric.js 5.3.1 + PDF.js 3.11.174 + Plotly.js 2.32.0 + Flask-Babel —
**tutte copie locali**, nessun CDN.
- **Deploy:** Docker Compose. Dev = Nginx; Prod = Traefik + Let's Encrypt.
- **Tooling:** uv, pytest + pytest-asyncio + httpx + aiosqlite.
## Decisioni architetturali rilevanti
| Decisione | Stato | Note |
|---|---|---|
| Frontend Flask invece di React (deroga vs spec §8) | **Confermata** | Tablet UX server-side, USB calipers/barcode, Fabric.js editor, i18n Babel collaudato. Vedi conversazione 2026-04-25. |
| NATS messaging (spec §7) | **Skippato** | Monorepo single-host, no microservizi. Nessuno stub `nats_client/` creato. |
| Envelope risposta `{success,data,error}` (spec §6) | **Rimandato** | Costo 4-5gg refactor + rotture client. Eventuale v2 API in M2. |
| Header `X-API-Key` vs spec `X-Api-Key` | **Mantenuto attuale** | Rinominare costa 50+ punti di codice + breaking per deploy. Rivedere in M2. |
| Variabili `.env` (DB_HOST, SERVER_PORT, ...) | **Mantenute attuali** | Rename a SERVICE_NAME/SERVICE_DOMAIN/API_KEY rinviato (impatta deploy esistenti). |
| Le regole di misura stanno sul server, non sullo schermo | **Confermata (V3.0.0)** | Tracciabilità, inserimento manuale, fuori tolleranza: la schermata può nasconderle, il server le rifiuta. |
| Una app di stazione per PC, dati sul server | **Decisa il 28/07** (D-3) | Validazione con l'IT di Tràfilo aperta come D-4. |
| Descrizioni in marcatura, non in HTML | **Confermata** | Niente HTML accettato in ingresso: la sanificazione è per costruzione, non per filtro. |
| Frontend Flask invece di React (deroga vs spec §8) | **Confermata** | Tablet UX server-side, calibri USB, editor Fabric.js, i18n Babel collaudato. |
| NATS messaging (spec §7) | **Skippato** | Monorepo single-host, nessun microservizio. |
| Envelope risposta `{success,data,error}` (spec §6) | **Rimandato** | Costo alto, rotture client. Eventuale API v2. |
| Header `X-API-Key` vs spec `X-Api-Key` | **Mantenuto attuale** | Rinominare è breaking per i deploy esistenti. |
## Branch git
- **Default:** `V2.0.0` (lavoro corrente)
- **Mantenuti:** `V1.0.0``V1.0.7` (release branches storiche)
- **Mergiato e chiuso:** `feature/rev04-phase1-stations` (in `V2.0.0` con commit `ea8e468`)
- **Corrente:** `V3.0.0`
- **Precedenti:** `V2.0.0`, `V1.0.0``V1.0.7` (release storiche)