commit dbdbb77daf362f315f0d1889c94d7bfa74b11585 Author: Adriano Date: Sat Feb 7 00:16:54 2026 +0100 feat: FASE 0 - Setup progetto TieMeasureFlow Struttura monorepo completa con server FastAPI e client Flask: - Server: FastAPI + SQLAlchemy 2.0 async + Alembic migrations - Client: Flask + blueprints (auth, measure, maker, statistics) - Database: docker-compose MySQL 8.0 + Alembic async config - Config: pydantic-settings, TailwindCSS, Flask-Babel i18n - Piano implementazione completo (18 sezioni, 1600 righe) Co-Authored-By: Claude Opus 4.6 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c6f9346 --- /dev/null +++ b/.env.example @@ -0,0 +1,30 @@ +# =========================== +# TieMeasureFlow Configuration +# =========================== + +# --- Database --- +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=tiemeasureflow +DB_USER=tmflow +DB_PASSWORD=change_me_in_production + +# --- Server --- +SERVER_HOST=0.0.0.0 +SERVER_PORT=8000 +SERVER_SECRET_KEY=change-this-to-a-random-secret-key +SERVER_CORS_ORIGINS=http://localhost:5000 + +# --- Client --- +CLIENT_HOST=0.0.0.0 +CLIENT_PORT=5000 +CLIENT_SECRET_KEY=change-this-to-another-random-secret-key +API_SERVER_URL=http://localhost:8000 + +# --- File Storage --- +UPLOAD_DIR=server/uploads +MAX_UPLOAD_SIZE_MB=50 + +# --- HTTPS (Production) --- +# SSL_CERTFILE=/path/to/cert.pem +# SSL_KEYFILE=/path/to/key.pem diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f47e58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +*.egg-info/ +dist/ +build/ +*.egg +.eggs/ + +# Virtual environments +venv/ +.venv/ +env/ + +# Environment +.env + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db +desktop.ini + +# Uploads (server-side files) +server/uploads/images/* +server/uploads/pdfs/* +server/uploads/logos/* +server/uploads/reports/* +!server/uploads/images/.gitkeep +!server/uploads/pdfs/.gitkeep +!server/uploads/logos/.gitkeep +!server/uploads/reports/.gitkeep + +# TailwindCSS output +client/static/css/tailwind.css + +# Node +node_modules/ + +# Flask-Babel compiled +*.mo + +# Alembic +server/migrations/versions/*.py +!server/migrations/versions/.gitkeep + +# Logs +*.log + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# Competitor analysis (local only) +Concorrente/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3e1f61a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,74 @@ +# TieMeasureFlow - Istruzioni Progetto + +## Panoramica +TieMeasureFlow by Tielogic - Sistema di gestione task per misurazioni con calibro manuale. +Monorepo con server FastAPI (backend API) e client Flask (frontend tablet). + +## Stack +- **Server**: FastAPI + SQLAlchemy 2.0 async + asyncmy + MySQL 8.0 +- **Client**: Flask 3.0 + Jinja2 + htmx 2.0 + Alpine.js 3.x + TailwindCSS 3.x +- **Grafici**: Plotly.js (browser) + Kaleido (server export SVG) +- **PDF**: WeasyPrint (HTML → PDF), PDF.js (rendering browser) +- **Annotazioni**: Fabric.js 6.x (editor Maker), Canvas overlay (display MeasurementTec) +- **i18n**: Flask-Babel (server) + alpinejs-i18n (client), lingue IT/EN +- **Auth**: API Key nell'header X-API-Key + +## Struttura +``` +TieMeasureFlow/ +├── server/ # FastAPI Backend (porta 8000) +│ ├── main.py # Entry point +│ ├── config.py # Settings da .env +│ ├── database.py # SQLAlchemy async engine +│ ├── models/ # SQLAlchemy ORM models +│ ├── schemas/ # Pydantic validation schemas +│ ├── routers/ # API endpoint routers +│ ├── services/ # Business logic +│ ├── middleware/ # API key auth, logging +│ └── migrations/ # Alembic migrations +├── client/ # Flask Frontend (porta 5000) +│ ├── app.py # Entry point +│ ├── blueprints/ # Flask route blueprints +│ ├── services/ # API client wrapper +│ ├── templates/ # Jinja2 HTML templates +│ ├── static/ # CSS, JS, immagini +│ └── translations/ # Flask-Babel i18n +└── docs/ # Documentazione +``` + +## Convenzioni Codice +- Python 3.11+, type hints ovunque +- async/await per tutte le operazioni DB (server) +- Pydantic v2 per validazione I/O API +- Nomi variabili e commenti in inglese, UI strings in IT/EN via i18n +- File imports ordinati: stdlib, third-party, local +- Nessun import wildcard (from x import *) + +## Database +- MySQL 8.0 con charset utf8mb4 +- SQLAlchemy 2.0 async con asyncmy driver +- Alembic per migrations +- Tabelle: users, recipes, recipe_versions, recipe_tasks, recipe_subtasks, measurements, access_logs, system_settings, recipe_version_audit + +## Ruoli Utente +- **Maker**: crea/modifica ricette +- **MeasurementTec**: esegue misurazioni +- **Metrologist**: visualizza statistiche SPC +- Ruoli combinabili (JSON array), flag `is_admin` separato + +## Pattern Importanti +- **Recipe Versioning**: copy-on-write immutabile. Modifiche creano nuova versione, misure restano legate a versione originale +- **Pass/Fail**: calcolato da tolleranze UTL/UWL/LWL/LTL su ogni subtask +- **File Storage**: uploads/{images,pdfs,logos,reports}/{recipe_id}/{version_id}/ + +## Comandi +- Server: `cd server && uvicorn main:app --reload --host 0.0.0.0 --port 8000` +- Client: `cd client && flask run --host 0.0.0.0 --port 5000` +- Migrations: `cd server && alembic upgrade head` +- TailwindCSS: `cd client && npx tailwindcss -i static/css/input.css -o static/css/tailwind.css --watch` + +## Brand +- Colore primario: #2563EB (blu industriale) +- Colore secondario: #64748B (grigio acciaio) +- Font UI: Inter +- Font numeri: JetBrains Mono diff --git a/PIANO_IMPLEMENTAZIONE.md b/PIANO_IMPLEMENTAZIONE.md new file mode 100644 index 0000000..6085429 --- /dev/null +++ b/PIANO_IMPLEMENTAZIONE.md @@ -0,0 +1,1599 @@ +# TieMeasureFlow - Piano di Implementazione Completo + +> **TieMeasureFlow** by Tielogic +> Sistema di gestione task per misurazioni con calibro manuale +> Data: 2026-02-06 | Versione Piano: 1.0 + +--- + +## Indice + +0. [Brand Identity](#0-brand-identity) +1. [Analisi Concorrenza](#1-analisi-concorrenza) +2. [Architettura Generale](#2-architettura-generale) +3. [Stack Tecnologico e Motivazioni](#3-stack-tecnologico-e-motivazioni) +4. [Struttura Monorepo](#4-struttura-monorepo) +5. [Modello Dati MySQL](#5-modello-dati-mysql) +6. [API REST Endpoints](#6-api-rest-endpoints) +7. [Ruoli e Autorizzazioni](#7-ruoli-e-autorizzazioni) +8. [UX Design e Wireframe](#8-ux-design-e-wireframe) +9. [Fasi di Sviluppo](#9-fasi-di-sviluppo) +10. [Dettaglio Task e Agenti](#10-dettaglio-task-e-agenti) +11. [Versioning Ricette](#11-versioning-ricette) +12. [Gestione File Server](#12-gestione-file-server) +13. [Tastierino Touch](#13-tastierino-touch) +14. [Internazionalizzazione e Temi](#14-internazionalizzazione-e-temi) +15. [Sicurezza e Rete](#15-sicurezza-e-rete) +16. [Evoluzioni Future](#16-evoluzioni-future) +17. [Decisioni Architetturali](#17-decisioni-architetturali) + +--- + +## 0. Brand Identity + +| Elemento | Valore | +|----------|--------| +| **Nome prodotto** | TieMeasureFlow | +| **Nome breve** | TMFlow | +| **Azienda** | Tielogic | +| **Claim** | "Tie" (da Tielogic) + "Measure" + "Flow" = collega le misure in un flusso guidato | +| **Concept logo** | Calibro stilizzato + freccia fluida curva | +| **Colore primario** | Blu industriale `#2563EB` | +| **Colore secondario** | Grigio acciaio `#64748B` | +| **Colore accent** | Blu scuro `#1E40AF` | +| **Tema light** | Sfondo `#F8FAFC`, testo `#0F172A` | +| **Tema dark** | Sfondo `#0F172A`, testo `#F1F5F9` | +| **Feedback misura OK** | Verde `#059669` | +| **Feedback misura WARNING** | Giallo `#D97706` | +| **Feedback misura FAIL** | Rosso `#DC2626` | +| **Font UI** | Inter | +| **Font numeri/misure** | JetBrains Mono (monospace, ottimo per valori decimali) | +| **Logo varianti** | Completo (icona+testo), icona sola, monocromo bianco, monocromo scuro | + +### Concept Logo + +``` + ┌──────────────────────────────────────────────┐ + │ │ + │ ╔══╗ │ + │ ║ ║──────┐ │ + │ ║ ║ │ ~~~> (freccia fluida curva) │ + │ ║ ║──────┘ │ + │ ╚══╝ │ + │ Calibro stilizzato Flow │ + │ │ + │ TieMeasureFlow │ + │ by Tielogic │ + │ │ + └──────────────────────────────────────────────┘ + + Varianti necessarie: + - Logo completo (icona + testo) per navbar e login + - Icona sola (favicon 32x32, 16x16) per tab browser + - Monocromo bianco (su sfondo scuro / dark theme) + - Monocromo scuro (su sfondo chiaro / light theme) + - Formato SVG (vettoriale) + PNG (raster) +``` + +### Branding Configurabile + +Il logo e il nome azienda sono **configurabili dal server**: +- Ogni installazione del sistema puo caricare il proprio logo +- Il nome azienda e visibile nella navbar e nei report PDF +- Configurazione via `system_settings` nel database +- Il branding "Powered by TieMeasureFlow - Tielogic" resta nel footer + +--- + +## 1. Analisi Concorrenza + +### Mitutoyo MeasurLink Real-Time Standard (v10) + +**Prezzo**: $1.680/licenza | **Tipo**: Desktop Windows + +#### Analisi Screenshot Concorrente + +**Vista Classica (std1.jpg)**: +- Grafici a barre per ogni caratteristica (Diametro Interno, Esterno, Lunghezza) +- Zone colorate: rosso (fuori tolleranza), giallo (warning), verde (in specifica) +- Foto del pezzo fisico + foto calibro che misura il pezzo +- Prompt guidato in basso: "Next: Coupling 256-78 Inside Diameter" +- Stato DAQ: "USB Input Tool" per acquisizione diretta + +**Carta di Controllo SPC (std2.jpg)**: +- Grafico con limiti UTL/UWL/Target/LWL/LTL a zone colorate +- Istogramma con indici Cp, Cpk, Pp, Ppk +- Tabella dati: osservazione, sottogruppo, valore, timestamp +- Tab per ogni caratteristica misurata + +**Vista Multi-caratteristica (std3.jpg)**: +- 5 "Position Offset" affiancati, ciascuno con carta di controllo + istogramma +- Indici di capability per ogni posizione +- Confronto visivo immediato tra posizioni + +#### Confronto Competitivo + +| Aspetto | MeasurLink | TieMeasureFlow (nostro) | Vantaggio | +|---------|------------|------------------------|-----------| +| **Piattaforma** | Desktop Windows | Web app (qualsiasi dispositivo) | Nostro | +| **Prezzo** | $1.680/licenza | Da definire (piu accessibile) | Nostro | +| **Guida visuale** | Foto generica pezzo | **Immagine/PDF annotata con marker numerati** | Nostro | +| **Dove misurare** | Lista piatta caratteristiche | **Frecce e indicatori posizionati sul disegno tecnico** | Nostro | +| **Workflow** | Semi-guidato | **Task strutturati con subtask per marker** | Nostro | +| **Selezione ricetta** | Manuale da lista | **Barcode + URL parametrico + manuale** | Nostro | +| **Multilingua** | Solo inglese | **IT/EN configurabile per utente** | Nostro | +| **Tema** | Un solo tema | **Dark/Light per utente** | Nostro | +| **SPC** | Completo, maturo | Completo (da implementare) | MeasurLink | +| **Integrazione HW** | Vasta gamma calibri | Web Serial API (Chrome/Edge) | MeasurLink | +| **Storicita** | Prodotto consolidato | Nuovo prodotto | MeasurLink | +| **Editor ricette** | Assente (setup tecnico) | **Editor drag-and-drop con Fabric.js** | Nostro | + +#### Feature Adottate da MeasurLink + +| Feature | Come la implementiamo | +|---------|----------------------| +| Zone colorate rosso/giallo/verde | Feedback in tempo reale su ogni misura inserita | +| Prompt "Next" guidato | Indicatore "Prossima misura: Marker #N - Descrizione" | +| Stato connessione DAQ | Icona stato calibro USB nella status bar | +| Tolleranze UTL/UWL/Target/LWL/LTL | Configurabili per ogni subtask nella ricetta | +| Timestamp automatico | Ogni misura registrata con data/ora precisa | +| Istogrammi e Cp/Cpk | Dashboard Metrologist con Plotly.js | + +--- + +## 2. Architettura Generale + +``` +┌──────────────────────────────────────────────────────────────┐ +│ TABLET WINDOWS (Edge/Chrome) │ +│ │ +│ ┌──────────────── FLASK CLIENT ──────────────────────────┐ │ +│ │ │ │ +│ │ [Logo Azienda] TieMeasureFlow [IT/EN] [Dark] [User] │ │ +│ │ ───────────────────────────────────────────────────── │ │ +│ │ │ │ +│ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │ │ +│ │ │ /maker │ │ /measure │ │ /statistics │ │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ │ Fabric.js │ │ PDF.js │ │ Plotly.js │ │ │ +│ │ │ Editor │ │ Canvas │ │ Carte controllo │ │ │ +│ │ │ Ricette │ │ Web Serial │ │ Istogrammi │ │ │ +│ │ │ Upload file │ │ Numpad touch │ │ Trend/Cp/Cpk │ │ │ +│ │ │ Task/Sub │ │ Barcode scan │ │ Report PDF │ │ │ +│ │ │ Versioning │ │ CSV export │ │ Filtri avanzati │ │ │ +│ │ └──────┬──────┘ └──────┬───────┘ └───────┬─────────┘ │ │ +│ │ └───────────────┼─────────────────┘ │ │ +│ └─────────────────────────┼───────────────────────────────┘ │ +│ │ │ +└─────────────────────────────┼──────────────────────────────────┘ + │ HTTPS + API Key + │ ZeroTier Network +┌─────────────────────────────┼──────────────────────────────────┐ +│ FastAPI SERVER │ +│ │ │ +│ ┌──────────────────────────┼────────────────────────────────┐ │ +│ │ /api/auth - Login, logout, profilo utente │ │ +│ │ /api/users - CRUD utenti + ruoli + API keys │ │ +│ │ /api/recipes - CRUD ricette + versioning immutabile │ │ +│ │ /api/tasks - Task + subtask per versione ricetta │ │ +│ │ /api/measurements - Salvataggio + query misure │ │ +│ │ /api/statistics - Aggregazioni SPC, indici capability │ │ +│ │ /api/files - Upload/download immagini/PDF │ │ +│ │ /api/reports - Generazione PDF (WeasyPrint + Kaleido) │ │ +│ │ /api/settings - Config sistema (logo, nome azienda) │ │ +│ └──────────────────────────┬────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────┐ ┌──────┴───────────────┐ │ +│ │ /uploads │ │ MySQL 8.0+ │ │ +│ │ ├── images/ │ │ │ │ +│ │ ├── pdfs/ │ │ users │ │ +│ │ ├── logos/ │ │ recipes │ │ +│ │ └── reports/ │ │ recipe_versions │ │ +│ │ │ │ recipe_tasks │ │ +│ │ (disco locale) │ │ recipe_subtasks │ │ +│ │ │ │ measurements │ │ +│ └────────────────┘ │ access_logs │ │ +│ │ system_settings │ │ +│ │ recipe_version_audit │ │ +│ └────────────────────────┘ │ +└───────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. Stack Tecnologico e Motivazioni + +### Backend + +| Tecnologia | Versione | Motivazione | +|-----------|----------|-------------| +| **FastAPI** | 0.110+ | Async nativo, auto-docs OpenAPI, performance per tablet multipli | +| **Uvicorn** | 0.30+ | ASGI server, supporto HTTPS | +| **SQLAlchemy 2.0** | 2.0+ | ORM async first-class, type-safe, moderne best practice | +| **asyncmy** | 0.2+ | Driver MySQL async (non-blocking I/O) | +| **MySQL** | 8.0+ | Richiesto dal cliente, JSON support, buon ecosistema | +| **Alembic** | 1.13+ | Migration database versionato | +| **Pydantic** | 2.0+ | Validazione dati, serializzazione automatica | +| **passlib + bcrypt** | - | Hash password sicuro | +| **Pillow** | 10+ | Generazione miniature immagini | +| **Plotly Python** | 5.x | Generazione grafici SPC server-side per report | +| **Kaleido** | 0.2+ | Export grafici Plotly in SVG/PNG ad alta qualita | +| **WeasyPrint** | 62+ | HTML → PDF professionale, supporta CSS3 moderno | + +### Frontend Client + +| Tecnologia | Versione | Motivazione | +|-----------|----------|-------------| +| **Flask** | 3.0+ | Lightweight, Jinja2 integrato, blueprint routing | +| **Jinja2** | 3.x | Template engine server-side, i18n nativo | +| **htmx** | 2.0 | HTML-over-the-wire, aggiornamenti parziali senza SPA | +| **Alpine.js** | 3.x | Reattivita client-side leggera, stato locale | +| **TailwindCSS** | 3.x | Utility-first, personalizzazione tema, dark mode nativa | +| **PDF.js** | 4.x | Rendering PDF in canvas, worker-based, HiDPI | +| **Fabric.js** | 6.x | Editor annotazioni: frecce, cerchi, rettangoli, drag-and-drop | +| **Plotly.js** | 5.x | Grafici SPC interattivi (zoom, hover, filtri) | +| **html5-qrcode** | 2.x | Barcode scanner via camera, cross-browser | +| **Web Serial API** | Browser | Lettura calibri USB, nativo Chrome/Edge | +| **Flask-Babel** | 4.x | i18n server-side (estrazione stringhe, .po/.mo) | +| **alpinejs-i18n** | 2.x | i18n client-side (switch lingua dinamico) | +| **Inter font** | - | UI professionale, leggibilita ottimale | +| **JetBrains Mono** | - | Monospace per valori numerici/misure | + +### Perche Queste Scelte + +| Scelta | Alternative scartate | Motivazione | +|--------|---------------------|-------------| +| FastAPI vs Django | Django REST troppo pesante per API pura | FastAPI e 3-5x piu veloce, async nativo | +| Flask vs React | React = SPA complessa, overhead JS | Flask + htmx = semplicita, SEO, meno bundle JS | +| TailwindCSS vs Bootstrap | Bootstrap meno personalizzabile | Tailwind: dark mode nativa, CSS variables, look moderno | +| Plotly vs Chart.js | Chart.js meno potente per SPC | Plotly: export SVG, piu tipi di grafici, hover ricchi | +| WeasyPrint vs wkhtmltopdf | wkhtmltopdf non supporta CSS3 | WeasyPrint: flexbox, grid, CSS moderno | +| Fabric.js vs marker.js | marker.js licenza commerciale | Fabric.js: MIT, oggetti ricchi, export JSON | +| MySQL vs PostgreSQL | Richiesto MySQL dal cliente | MySQL 8.0+ ha JSON, async, sufficiente | + +--- + +## 4. Struttura Monorepo + +``` +TieMeasureFlow/ +├── PIANO_IMPLEMENTAZIONE.md # Questo file +├── CLAUDE.md # Istruzioni progetto per Claude +├── docker-compose.yml # MySQL + servizi (opzionale) +├── .env.example # Template variabili ambiente +├── .gitignore +│ +├── server/ # FastAPI Backend +│ ├── main.py # Entry point FastAPI +│ ├── requirements.txt +│ ├── config.py # Configurazione (DB, paths, API keys) +│ ├── database.py # Engine SQLAlchemy async +│ │ +│ ├── models/ # SQLAlchemy Models +│ │ ├── __init__.py +│ │ ├── user.py # User + roles (JSON) +│ │ ├── recipe.py # Recipe + RecipeVersion (immutabile) +│ │ ├── task.py # RecipeTask + RecipeSubtask +│ │ ├── measurement.py # Measurement records +│ │ ├── access_log.py # Login/action logs +│ │ └── setting.py # SystemSetting (logo, nome, config) +│ │ +│ ├── schemas/ # Pydantic Schemas (validazione I/O) +│ │ ├── __init__.py +│ │ ├── user.py +│ │ ├── recipe.py # Include version schemas +│ │ ├── task.py # Include subtask schemas +│ │ ├── measurement.py +│ │ └── statistics.py # SPC response schemas +│ │ +│ ├── routers/ # API Routers (endpoint grouping) +│ │ ├── __init__.py +│ │ ├── auth.py # POST login, logout, GET/PUT me +│ │ ├── users.py # CRUD utenti (admin only) +│ │ ├── recipes.py # CRUD ricette + versioning +│ │ ├── tasks.py # CRUD task + subtask +│ │ ├── measurements.py # POST misure, GET query +│ │ ├── statistics.py # GET SPC data endpoints +│ │ ├── files.py # POST upload, GET download +│ │ ├── reports.py # GET genera PDF report +│ │ └── settings.py # GET/PUT config sistema +│ │ +│ ├── services/ # Business Logic (separata dai router) +│ │ ├── __init__.py +│ │ ├── auth_service.py # Login, password hash, API key validation +│ │ ├── recipe_service.py # Versioning copy-on-write logic +│ │ ├── measurement_service.py # Salvataggio, pass/fail calc, deviation +│ │ ├── spc_service.py # Calcoli Cp, Cpk, Pp, Ppk, X-bar, R +│ │ └── report_service.py # WeasyPrint + Plotly/Kaleido generation +│ │ +│ ├── middleware/ # Middleware FastAPI +│ │ ├── __init__.py +│ │ ├── api_key.py # Verifica API Key su ogni request +│ │ └── logging.py # Access logging automatico +│ │ +│ ├── templates/ # Jinja2 template per report PDF server-side +│ │ └── reports/ +│ │ ├── base_report.html # Layout base report con logo +│ │ ├── spc_report.html # Report SPC con grafici +│ │ └── measurement_report.html # Report misure tabulare +│ │ +│ ├── uploads/ # File uploadati (GITIGNORED) +│ │ ├── images/ # {recipe_id}/{version_id}/ +│ │ ├── pdfs/ # {recipe_id}/{version_id}/ +│ │ ├── logos/ # Logo azienda configurabile +│ │ └── reports/ # Report PDF temporanei (auto-pulizia 24h) +│ │ +│ ├── migrations/ # Alembic DB migrations +│ │ ├── env.py +│ │ ├── alembic.ini +│ │ └── versions/ +│ │ +│ └── tests/ +│ ├── conftest.py # Fixtures condivise (test DB, client) +│ ├── test_auth.py +│ ├── test_recipes.py # Include test versioning +│ ├── test_measurements.py +│ └── test_spc.py # Test calcoli statistici +│ +├── client/ # Flask Frontend (Tablet UI) +│ ├── app.py # Entry point Flask +│ ├── requirements.txt +│ ├── config.py # URL server API, API key, settings +│ │ +│ ├── blueprints/ # Flask Blueprints (routing per ruolo) +│ │ ├── __init__.py +│ │ ├── auth.py # Login/logout/profile pages +│ │ ├── measure.py # Pagine MeasurementTec +│ │ ├── maker.py # Pagine Maker (editor ricette) +│ │ └── statistics.py # Pagine Metrologist (SPC) +│ │ +│ ├── services/ # API client helpers +│ │ ├── __init__.py +│ │ └── api_client.py # Wrapper requests verso FastAPI server +│ │ +│ ├── templates/ # Jinja2 Templates +│ │ ├── base.html # Layout: navbar, footer, tema, lingua, logo +│ │ │ +│ │ ├── components/ # Componenti riutilizzabili +│ │ │ ├── navbar.html # Navbar dinamica per ruoli + logo + lingua + tema +│ │ │ ├── numpad.html # Tastierino touch per misure manuali +│ │ │ ├── caliper_status.html # Indicatore stato connessione calibro USB +│ │ │ ├── barcode_scanner.html # Modal scanner barcode +│ │ │ ├── measurement_feedback.html # Feedback colore verde/giallo/rosso +│ │ │ └── next_measurement.html # Indicatore "Prossima misura" +│ │ │ +│ │ ├── auth/ +│ │ │ ├── login.html # Login con logo azienda +│ │ │ └── profile.html # Modifica: nome display, lingua, tema +│ │ │ +│ │ ├── measure/ # Pagine ruolo MeasurementTec +│ │ │ ├── select_recipe.html # Selezione ricetta (barcode/manual/URL param) +│ │ │ ├── task_list.html # Lista task con miniature + progress +│ │ │ ├── task_execute.html # Esecuzione: immagine annotata + numpad + misure +│ │ │ └── task_complete.html # Riepilogo: pass/fail + export CSV +│ │ │ +│ │ ├── maker/ # Pagine ruolo Maker +│ │ │ ├── recipe_list.html # Lista ricette con filtri + stato versione +│ │ │ ├── recipe_editor.html # Editor: form + upload + Fabric.js canvas +│ │ │ ├── task_editor.html # Editor task/subtask con tolleranze +│ │ │ ├── recipe_preview.html # Anteprima come vista MeasurementTec +│ │ │ └── version_history.html # Storico versioni con diff +│ │ │ +│ │ └── statistics/ # Pagine ruolo Metrologist +│ │ ├── dashboard.html # Dashboard SPC: overview + alert +│ │ ├── control_chart.html # Carta X-bar / R interattiva +│ │ ├── histogram.html # Istogramma + curva normale + distribuzione +│ │ ├── capability.html # Gauge Cp/Cpk/Pp/Ppk con indicatori +│ │ ├── trend.html # Trend temporali + confronto periodi +│ │ └── filters.html # Componente filtri: date, ricetta, operatore, lotto, seriale +│ │ +│ ├── static/ +│ │ ├── css/ +│ │ │ ├── tailwind.css # TailwindCSS compilato +│ │ │ ├── themes.css # CSS Variables per dark/light + brand colors +│ │ │ └── numpad.css # Stili tastierino touch (bottoni grandi) +│ │ │ +│ │ ├── js/ +│ │ │ ├── alpine-init.js # Alpine.js config + i18n + tema toggle +│ │ │ ├── caliper.js # Web Serial API: connect, read, parse dati calibro +│ │ │ ├── barcode.js # html5-qrcode: init camera, decode, callback +│ │ │ ├── numpad.js # Logica numpad: cifre, punto, +/-, backspace, invio +│ │ │ ├── annotation-viewer.js # Canvas overlay display-only (MeasurementTec) +│ │ │ ├── annotation-editor.js # Fabric.js editor completo (Maker) +│ │ │ ├── csv-export.js # Export misure in CSV (Blob API) +│ │ │ └── spc-charts.js # Plotly.js: configurazioni grafici SPC +│ │ │ +│ │ ├── img/ +│ │ │ ├── tmflow-logo.svg # Logo TieMeasureFlow (vettoriale) +│ │ │ ├── tmflow-icon.svg # Icona sola (favicon source) +│ │ │ ├── favicon.ico # Favicon 32x32 +│ │ │ └── default-company-logo.png # Logo placeholder se non configurato +│ │ │ +│ │ └── vendor/ # Librerie JS locali (fallback CDN) +│ │ ├── pdf.js/ +│ │ ├── fabric.js/ +│ │ ├── plotly.js/ +│ │ ├── htmx.min.js +│ │ ├── alpine.min.js +│ │ └── html5-qrcode/ +│ │ +│ ├── translations/ # Flask-Babel i18n +│ │ ├── babel.cfg # Config estrazione stringhe +│ │ ├── messages.pot # Template stringhe estratte +│ │ ├── it/LC_MESSAGES/ +│ │ │ ├── messages.po # Traduzioni italiano +│ │ │ └── messages.mo # Compilato +│ │ └── en/LC_MESSAGES/ +│ │ ├── messages.po # Traduzioni inglese +│ │ └── messages.mo # Compilato +│ │ +│ └── tests/ +│ ├── test_auth_pages.py +│ ├── test_measure_flow.py +│ └── test_maker_flow.py +│ +└── docs/ # Documentazione + ├── API.md # Documentazione API REST completa + ├── DEPLOYMENT.md # Guida deployment (server + client + MySQL) + ├── USER_GUIDE.md # Guida utente per tutti i ruoli + └── COMPETITOR_ANALYSIS.md # Analisi dettagliata concorrenza +``` + +--- + +## 5. Modello Dati MySQL + +### Diagramma ER + +``` +┌──────────────┐ ┌────────────────┐ ┌──────────────────┐ +│ users │ │ recipes │ │ system_settings │ +│──────────────│ │────────────────│ │──────────────────│ +│ id (PK) │ │ id (PK) │ │ key (PK) │ +│ username │ │ code (UNIQUE) │ │ value │ +│ password_hash│ │ name │ │ type │ +│ email │ │ description │ │ description │ +│ display_name │ │ created_by (FK)─┼───┐ │ updated_at │ +│ roles (JSON) │ ┌───┤ created_at │ │ │ updated_by (FK) │ +│ language_pref│ │ │ active │ │ └──────────────────┘ +│ theme_pref │ │ └───────┬────────┘ │ +│ is_admin │ │ │ 1:N │ +│ active │ │ ┌───────┴────────┐ │ +│ api_key │ │ │recipe_versions │ │ +│ created_at │ │ │────────────────│ │ +│ last_login │ │ │ id (PK) │ │ +└──────┬───────┘ │ │ recipe_id (FK) │ │ + │ │ │ version_number │ │ + │ │ │ is_current │ │ + │ └───┤ created_by (FK) │ │ + │ │ created_at │ │ + │ │ change_notes │ │ + │ └───────┬────────┘ │ + │ │ 1:N │ + │ ┌───────┴────────┐ │ + │ │ recipe_tasks │ │ + │ │────────────────│ │ + │ │ id (PK) │ │ + │ │ version_id (FK) │ │ + │ │ order_index │ │ + │ │ title │ │ + │ │ directive │ │ + │ │ description │ │ + │ │ file_path │ │ + │ │ file_type │ │ + │ │ annotations_json│ │ + │ └───────┬────────┘ │ + │ │ 1:N │ + │ ┌───────┴────────┐ │ + │ │recipe_subtasks │ │ + │ │────────────────│ │ + │ │ id (PK) │ │ + │ │ task_id (FK) │ │ + │ │ marker_number │ │ + │ │ description │ │ + │ │ measurement_type│ │ + │ │ nominal │ │ + │ │ utl, uwl │ │ + │ │ lwl, ltl │ │ + │ │ unit │ │ + │ └───────┬────────┘ │ + │ │ 1:N │ + │ ┌───────┴──────────┐ │ + └───────────────┤ measurements │ │ + │──────────────────│ │ + │ id (PK BIGINT) │ │ + │ subtask_id (FK) │ │ + │ version_id (FK) │ │ + │ measured_by (FK)──┘ │ + │ value │ + │ pass_fail │ + │ deviation │ + │ lot_number │ + │ serial_number │ + │ input_method │ + │ measured_at │ + │ synced_to_csv │ + └───────────────────┘ + +┌──────────────────────┐ ┌──────────────────────────┐ +│ access_logs │ │ recipe_version_audit │ +│──────────────────────│ │──────────────────────────│ +│ id (PK BIGINT) │ │ id (PK BIGINT) │ +│ user_id (FK) │ │ recipe_id (FK) │ +│ action │ │ old_version_id (FK NULL) │ +│ details (JSON) │ │ new_version_id (FK) │ +│ ip_address │ │ changed_by (FK) │ +│ user_agent │ │ change_type │ +│ created_at │ │ change_reason │ +└──────────────────────┘ │ created_at │ + └──────────────────────────┘ +``` + +### Schema SQL Dettagliato + +```sql +-- ============================================= +-- UTENTI +-- ============================================= +CREATE TABLE users ( + id INT PRIMARY KEY AUTO_INCREMENT, + username VARCHAR(100) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + email VARCHAR(255), + display_name VARCHAR(255) NOT NULL, + -- Ruoli: combinazione di "Maker", "MeasurementTec", "Metrologist" + roles JSON NOT NULL DEFAULT '[]', + -- Flag admin separato (almeno 1 utente deve essere admin) + is_admin BOOLEAN NOT NULL DEFAULT FALSE, + -- Preferenze personali configurabili dall'utente + language_pref ENUM('it', 'en') NOT NULL DEFAULT 'it', + theme_pref ENUM('light', 'dark') NOT NULL DEFAULT 'light', + -- Stato + active BOOLEAN NOT NULL DEFAULT TRUE, + api_key VARCHAR(64) UNIQUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login TIMESTAMP NULL, + INDEX idx_username (username), + INDEX idx_api_key (api_key) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================= +-- RICETTE (Master - metadati condivisi tra versioni) +-- ============================================= +CREATE TABLE recipes ( + id INT PRIMARY KEY AUTO_INCREMENT, + code VARCHAR(100) NOT NULL UNIQUE, -- Codice ricetta (per barcode) + name VARCHAR(255) NOT NULL, + description TEXT, + created_by INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + active BOOLEAN NOT NULL DEFAULT TRUE, + FOREIGN KEY (created_by) REFERENCES users(id), + INDEX idx_code (code), + INDEX idx_active (active) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================= +-- VERSIONI RICETTA (Immutabili - copy-on-write) +-- Ogni modifica crea una nuova versione. +-- Le misure restano legate alla versione originale. +-- ============================================= +CREATE TABLE recipe_versions ( + id INT PRIMARY KEY AUTO_INCREMENT, + recipe_id INT NOT NULL, + version_number INT NOT NULL, -- Auto-incrementale per ricetta + is_current BOOLEAN NOT NULL DEFAULT FALSE, + -- Audit + created_by INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + change_notes TEXT, -- Motivo modifica + FOREIGN KEY (recipe_id) REFERENCES recipes(id), + FOREIGN KEY (created_by) REFERENCES users(id), + UNIQUE KEY uk_recipe_version (recipe_id, version_number), + INDEX idx_current (recipe_id, is_current) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================= +-- TASK (legati a una versione specifica della ricetta) +-- ============================================= +CREATE TABLE recipe_tasks ( + id INT PRIMARY KEY AUTO_INCREMENT, + version_id INT NOT NULL, + order_index INT NOT NULL DEFAULT 0, -- Ordine visualizzazione + title VARCHAR(255) NOT NULL, + directive TEXT, -- Direttiva breve (es: "Misurare diametro interno") + description TEXT, -- Descrizione estesa + file_path VARCHAR(500), -- Path relativo file immagine/PDF + file_type ENUM('image', 'pdf') NULL, + annotations_json JSON, -- Fabric.js canvas export (frecce, marker, aree) + FOREIGN KEY (version_id) REFERENCES recipe_versions(id), + INDEX idx_version_order (version_id, order_index) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================= +-- SUBTASK (indicatori numerici posizionati su immagine) +-- Ogni marker numerico corrisponde a un punto di misura +-- ============================================= +CREATE TABLE recipe_subtasks ( + id INT PRIMARY KEY AUTO_INCREMENT, + task_id INT NOT NULL, + marker_number INT NOT NULL, -- Numero indicatore su immagine (1, 2, 3...) + description VARCHAR(500) NOT NULL, -- Es: "Diametro interno foro A" + measurement_type VARCHAR(100), -- Es: "diametro", "lunghezza", "profondita", "angolo" + nominal DECIMAL(12,6), -- Valore nominale target + utl DECIMAL(12,6), -- Upper Tolerance Limit (limite superiore) + uwl DECIMAL(12,6), -- Upper Warning Limit (warning superiore) + lwl DECIMAL(12,6), -- Lower Warning Limit (warning inferiore) + ltl DECIMAL(12,6), -- Lower Tolerance Limit (limite inferiore) + unit VARCHAR(20) NOT NULL DEFAULT 'mm', -- Unita: mm, inch, deg, etc. + FOREIGN KEY (task_id) REFERENCES recipe_tasks(id) ON DELETE CASCADE, + UNIQUE KEY uk_task_marker (task_id, marker_number), + INDEX idx_task (task_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================= +-- MISURAZIONI +-- Ogni record = una singola misura effettuata +-- ============================================= +CREATE TABLE measurements ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + subtask_id INT NOT NULL, -- Quale subtask e stato misurato + version_id INT NOT NULL, -- Link DIRETTO alla versione ricetta (immutabile) + measured_by INT NOT NULL, -- Chi ha misurato (MeasurementTec) + -- Dati misura + value DECIMAL(12,6) NOT NULL, + pass_fail ENUM('pass', 'warning', 'fail') NOT NULL, + deviation DECIMAL(12,6), -- value - nominal + -- Tracciabilita + lot_number VARCHAR(100), -- Numero lotto produzione + serial_number VARCHAR(100), -- Numero seriale pezzo + -- Metodo input + input_method ENUM('usb_caliper', 'manual') NOT NULL DEFAULT 'manual', + -- Timestamp + measured_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + -- Sync flag per CSV + synced_to_csv BOOLEAN DEFAULT FALSE, + FOREIGN KEY (subtask_id) REFERENCES recipe_subtasks(id), + FOREIGN KEY (version_id) REFERENCES recipe_versions(id), + FOREIGN KEY (measured_by) REFERENCES users(id), + INDEX idx_version (version_id), + INDEX idx_subtask (subtask_id), + INDEX idx_measured_at (measured_at), + INDEX idx_lot (lot_number), + INDEX idx_serial (serial_number), + INDEX idx_operator (measured_by) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================= +-- LOG ACCESSI +-- ============================================= +CREATE TABLE access_logs ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + user_id INT, + action VARCHAR(100) NOT NULL, -- login, logout, recipe_create, recipe_update, etc. + details JSON, -- Dettagli aggiuntivi (es: recipe_id, version_id) + ip_address VARCHAR(45), -- IPv4 o IPv6 + user_agent VARCHAR(500), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id), + INDEX idx_user_time (user_id, created_at), + INDEX idx_action (action) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================= +-- IMPOSTAZIONI SISTEMA (key-value configurabile) +-- ============================================= +CREATE TABLE system_settings ( + setting_key VARCHAR(100) PRIMARY KEY, + setting_value TEXT NOT NULL, + setting_type ENUM('string', 'number', 'boolean', 'json') DEFAULT 'string', + description VARCHAR(500), + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + updated_by INT, + FOREIGN KEY (updated_by) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Dati iniziali settings +INSERT INTO system_settings (setting_key, setting_value, setting_type, description) VALUES +('company_name', 'TieMeasureFlow', 'string', 'Nome azienda visualizzato in navbar e report'), +('company_logo_path', '', 'string', 'Path logo azienda (relativo a uploads/logos/)'), +('max_upload_size_mb', '50', 'number', 'Dimensione massima upload file in MB'), +('csv_export_delimiter', ';', 'string', 'Delimitatore CSV per export'), +('csv_export_decimal', ',', 'string', 'Separatore decimale CSV per export'), +('default_language', 'it', 'string', 'Lingua default per nuovi utenti'), +('default_theme', 'light', 'string', 'Tema default per nuovi utenti'); + +-- ============================================= +-- AUDIT VERSIONI RICETTA +-- Traccia ogni modifica alle ricette +-- ============================================= +CREATE TABLE recipe_version_audit ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + recipe_id INT NOT NULL, + old_version_id INT, -- NULL se prima creazione + new_version_id INT NOT NULL, + changed_by INT NOT NULL, + change_type ENUM('CREATE', 'UPDATE', 'ACTIVATE', 'RETIRE') NOT NULL, + change_reason TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (recipe_id) REFERENCES recipes(id), + FOREIGN KEY (changed_by) REFERENCES users(id), + INDEX idx_recipe_time (recipe_id, created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +--- + +## 6. API REST Endpoints + +### Autenticazione & Utenti + +| Metodo | Endpoint | Descrizione | Ruolo | +|--------|----------|-------------|-------| +| POST | `/api/auth/login` | Login utente (username + password) | Tutti | +| POST | `/api/auth/logout` | Logout (invalida sessione) | Tutti | +| GET | `/api/auth/me` | Profilo corrente con ruoli | Tutti | +| PUT | `/api/auth/me` | Aggiorna profilo (display_name, lingua, tema) | Tutti | +| GET | `/api/users` | Lista utenti con ruoli | Admin | +| POST | `/api/users` | Crea utente con ruoli | Admin | +| PUT | `/api/users/{id}` | Modifica utente (ruoli, stato, admin) | Admin | +| DELETE | `/api/users/{id}` | Disattiva utente (soft delete) | Admin | +| POST | `/api/users/{id}/regenerate-key` | Rigenera API key utente | Admin | + +### Ricette + +| Metodo | Endpoint | Descrizione | Ruolo | +|--------|----------|-------------|-------| +| GET | `/api/recipes` | Lista ricette attive (paginata) | Tutti | +| GET | `/api/recipes/{id}` | Dettaglio ricetta con versione corrente + task | Tutti | +| GET | `/api/recipes/code/{code}` | Ricetta per codice barcode | MeasurementTec | +| POST | `/api/recipes` | Crea nuova ricetta (versione 1) | Maker | +| PUT | `/api/recipes/{id}` | Aggiorna ricetta (crea nuova versione) | Maker | +| DELETE | `/api/recipes/{id}` | Disattiva ricetta | Maker | +| GET | `/api/recipes/{id}/versions` | Lista tutte le versioni | Maker, Metrologist | +| GET | `/api/recipes/{id}/versions/{v}` | Dettaglio versione specifica | Maker, Metrologist | +| GET | `/api/recipes/{id}/versions/{v}/measurement-count` | Conteggio misure per versione | Maker | + +### Task & Subtask + +| Metodo | Endpoint | Descrizione | Ruolo | +|--------|----------|-------------|-------| +| GET | `/api/recipes/{id}/tasks` | Lista task versione corrente (ordinati) | Tutti | +| POST | `/api/recipes/{id}/tasks` | Aggiungi task (crea nuova versione) | Maker | +| PUT | `/api/tasks/{id}` | Modifica task | Maker | +| DELETE | `/api/tasks/{id}` | Elimina task | Maker | +| PUT | `/api/tasks/reorder` | Riordina task (drag-and-drop) | Maker | +| POST | `/api/tasks/{id}/subtasks` | Aggiungi subtask con tolleranze | Maker | +| PUT | `/api/subtasks/{id}` | Modifica subtask | Maker | +| DELETE | `/api/subtasks/{id}` | Elimina subtask | Maker | + +### Misurazioni + +| Metodo | Endpoint | Descrizione | Ruolo | +|--------|----------|-------------|-------| +| POST | `/api/measurements` | Salva singola misura | MeasurementTec | +| POST | `/api/measurements/batch` | Salva batch di misure | MeasurementTec | +| GET | `/api/measurements` | Query misure (con filtri paginati) | Metrologist | +| GET | `/api/measurements/export/csv` | Export CSV server-side | MeasurementTec, Metrologist | + +### Statistiche SPC + +| Metodo | Endpoint | Descrizione | Ruolo | +|--------|----------|-------------|-------| +| GET | `/api/statistics/control-chart` | Dati carta di controllo X-bar/R | Metrologist | +| GET | `/api/statistics/histogram` | Dati istogramma + distribuzione | Metrologist | +| GET | `/api/statistics/capability` | Indici Cp/Cpk/Pp/Ppk | Metrologist | +| GET | `/api/statistics/trend` | Trend temporali indici | Metrologist | +| GET | `/api/statistics/summary` | Riepilogo pass/fail/warning count | Metrologist | +| GET | `/api/statistics/compare-versions` | Confronto SPC tra versioni ricetta | Metrologist | +| GET | `/api/statistics/alerts` | Western Electric rules violations | Metrologist | + +**Filtri comuni per tutti gli endpoint statistics** (query params): +- `recipe_id` - Ricetta specifica +- `version_id` - Versione specifica (opzionale, default=tutte) +- `subtask_id` - Subtask specifico +- `date_from`, `date_to` - Intervallo date +- `operator_id` - MeasurementTec specifico +- `lot_number` - Numero lotto +- `serial_number` - Numero seriale + +### File & Report + +| Metodo | Endpoint | Descrizione | Ruolo | +|--------|----------|-------------|-------| +| POST | `/api/files/upload` | Upload immagine/PDF per task | Maker | +| GET | `/api/files/{path}` | Download/serve file | Tutti | +| DELETE | `/api/files/{path}` | Elimina file | Maker | +| GET | `/api/reports/spc` | Genera PDF report SPC (filtri come statistics) | Metrologist | +| GET | `/api/reports/measurements` | Genera PDF report misure tabulare | Metrologist | + +### Settings + +| Metodo | Endpoint | Descrizione | Ruolo | +|--------|----------|-------------|-------| +| GET | `/api/settings` | Leggi tutte le impostazioni | Tutti | +| PUT | `/api/settings` | Aggiorna impostazioni (batch) | Admin | +| POST | `/api/settings/logo` | Upload logo azienda | Admin | + +--- + +## 7. Ruoli e Autorizzazioni + +### Matrice Ruoli + +| Funzionalita | MeasurementTec | Maker | Metrologist | Admin | +|--------------|:-:|:-:|:-:|:-:| +| Login / Logout / Profilo personale | X | X | X | X | +| Modificare: nome display, lingua, tema | X | X | X | X | +| Seleziona ricetta (barcode/manual/URL) | X | | | | +| Visualizza task con immagine annotata | X | | | | +| Inserisce misure (numpad / calibro USB) | X | | | | +| Vede feedback colore (pass/warning/fail) | X | | | | +| Export CSV misure | X | | | | +| Crea nuove ricette | | X | | | +| Modifica ricette (crea nuova versione) | | X | | | +| Upload immagini / PDF | | X | | | +| Editor annotazioni Fabric.js | | X | | | +| Gestione task e subtask con tolleranze | | X | | | +| Anteprima ricetta come MeasurementTec | | X | | | +| Storico versioni ricetta | | X | X | | +| Dashboard SPC overview | | | X | | +| Carte di controllo X-bar / R | | | X | | +| Istogrammi e distribuzione | | | X | | +| Indici capability Cp/Cpk/Pp/Ppk | | | X | | +| Trend temporali e confronto periodi | | | X | | +| Alert Western Electric rules | | | X | | +| Download report PDF | | | X | | +| Confronto SPC tra versioni ricetta | | | X | | +| Gestione utenti (CRUD) | | | | X | +| Impostazioni sistema (logo, nome, config) | | | | X | + +### Regole Combinazione Ruoli + +- Un utente puo avere **1, 2 o 3 ruoli** contemporaneamente (JSON array) +- `is_admin` e un **flag separato** (non un ruolo), combinabile con qualsiasi ruolo +- La navbar mostra **solo le voci** accessibili ai ruoli dell'utente corrente +- Esempio: utente con `roles: ["Maker", "Metrologist"], is_admin: true` vede tutto +- **Almeno 1 utente** deve essere admin (vincolo applicativo) +- Un utente **senza ruoli** puo solo fare login e modificare il proprio profilo + +--- + +## 8. UX Design e Wireframe + +### Pagina Login + +``` +┌──────────────────────────────────────────────┐ +│ │ +│ [Logo Azienda Configurabile] │ +│ ────────────────────────── │ +│ TieMeasureFlow │ +│ by Tielogic │ +│ │ +│ ┌──────────────────────────────┐ │ +│ │ Username │ │ +│ └──────────────────────────────┘ │ +│ ┌──────────────────────────────┐ │ +│ │ Password │ │ +│ └──────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────┐ │ +│ │ ACCEDI │ │ +│ └──────────────────────────────┘ │ +│ │ +│ [IT] [EN] [Dark] │ +└──────────────────────────────────────────────┘ +``` + +### Navbar (dinamica per ruoli) + +``` +┌──────────────────────────────────────────────────────────────┐ +│ [Logo] TieMeasureFlow | Misure | Ricette | Statistiche | │ +│ └──MeT──┘ └─Maker─┘ └─Metrolog──┘ │ +│ [IT/EN] [☀/🌙] [👤]│ +└──────────────────────────────────────────────────────────────┘ + Voci visibili in base ai ruoli dell'utente loggato +``` + +### MeasurementTec - Esecuzione Task + +``` +┌──────────────────────────────────────────────────────────────┐ +│ [Navbar] [Calibro: ●] │ +├──────────────────────────────────────────────────────────────┤ +│ │ +│ Task 2 di 5: Controllo Diametri Foro │ +│ Direttiva: Misurare i diametri indicati nel disegno │ +│ │ +│ ┌──────────────────────────────┐ ┌──────────────────────┐ │ +│ │ │ │ │ │ +│ │ [Immagine/PDF con │ │ Marker #2 │ │ +│ │ annotazioni overlay] │ │ Diametro Int. (mm) │ │ +│ │ │ │ Nom: 12.500 │ │ +│ │ ①──→ │ │ Tol: ±0.050 │ │ +│ │ ②──→ │ │ │ │ +│ │ ③──→ │ │ ┌────────────────┐ │ │ +│ │ │ │ │ 12.487 │ │ │ +│ │ [Rettangolo area] │ │ └────────────────┘ │ │ +│ │ │ │ ██████████████░░░░ │ │ +│ │ │ │ (barra verde OK) │ │ +│ └──────────────────────────────┘ │ │ │ +│ │ ┌───┬───┬───┬───┐ │ │ +│ │ │ 7 │ 8 │ 9 │ ⌫ │ │ │ +│ │ ├───┼───┼───┼───┤ │ │ +│ │ │ 4 │ 5 │ 6 │ C │ │ │ +│ │ ├───┼───┼───┼───┤ │ │ +│ │ │ 1 │ 2 │ 3 │+/-│ │ │ +│ │ ├───┼───┼───┼───┤ │ │ +│ │ │ 0 │ . │ │ ✓ │ │ │ +│ │ └───┴───┴───┴───┘ │ │ +│ │ │ │ +│ │ → Prossima: #3 Lung.│ │ +│ └──────────────────────┘ │ +│ │ +│ [← Task precedente] [Task successivo →] │ +└──────────────────────────────────────────────────────────────┘ +``` + +### Maker - Editor Annotazioni + +``` +┌──────────────────────────────────────────────────────────────┐ +│ [Navbar] │ +├──────────────────────────────────────────────────────────────┤ +│ │ +│ Ricetta: COUPLING-256 (v3) [Salva] [Anteprima] [Annulla] │ +│ │ +│ ┌── Toolbar Fabric.js ──────────────────────────────────┐ │ +│ │ [Freccia] [Marker#] [Rettangolo] [Testo] [Elimina] │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────┐ ┌──────────────────────┐ │ +│ │ │ │ Task 2: Diametri │ │ +│ │ [Canvas Fabric.js] │ │ │ │ +│ │ Immagine/PDF con │ │ Subtask (per marker):│ │ +│ │ annotazioni editabili │ │ │ │ +│ │ │ │ #1 Diam. Esterno │ │ +│ │ Drag frecce, marker, │ │ Nom: 25.000 mm │ │ +│ │ rettangoli area │ │ Tol: ±0.100 │ │ +│ │ │ │ [Modifica] [X] │ │ +│ │ │ │ │ │ +│ │ │ │ #2 Diam. Interno │ │ +│ │ │ │ Nom: 12.500 mm │ │ +│ │ │ │ Tol: ±0.050 │ │ +│ │ │ │ [Modifica] [X] │ │ +│ │ │ │ │ │ +│ └──────────────────────────────┘ │ [+ Aggiungi Subtask] │ │ +│ └──────────────────────┘ │ +│ │ +│ [Upload nuova immagine/PDF] │ +└──────────────────────────────────────────────────────────────┘ +``` + +### Metrologist - Dashboard SPC + +``` +┌──────────────────────────────────────────────────────────────┐ +│ [Navbar] │ +├──────────────────────────────────────────────────────────────┤ +│ │ +│ ┌── Filtri ───────────────────────────────────────────────┐ │ +│ │ Ricetta: [COUPLING-256 ▼] Versione: [Tutte ▼] │ │ +│ │ Dal: [2026-01-01] Al: [2026-02-06] │ │ +│ │ Operatore: [Tutti ▼] Lotto: [________] Seriale: [__] │ │ +│ │ [Applica filtri] │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────┐ ┌──────────────────────────────┐ │ +│ │ Riepilogo │ │ Capability │ │ +│ │ Misure: 1.247 │ │ │ │ +│ │ Pass: 1.198 (96.1%) │ │ Cp: 1.45 Cpk: 1.32 │ │ +│ │ Warn: 31 (2.5%) │ │ Pp: 1.41 Ppk: 1.28 │ │ +│ │ Fail: 18 (1.4%) │ │ │ │ +│ └─────────────────────┘ │ [Gauge indicator ████████░░] │ │ +│ └──────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ Carta di Controllo X-bar / R [Plotly.js] │ │ +│ │ ═══════════════════ UTL ═════════════════ │ │ +│ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ UWL ─ ─ ─ ─ ─ ─ ─ ─ │ │ +│ │ · · · · · · · · · Target │ │ +│ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ LWL ─ ─ ─ ─ ─ ─ ─ ─ │ │ +│ │ ═══════════════════ LTL ═════════════════ │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ +│ ┌────────────────────┐ ┌────────────────────────────────┐ │ +│ │ Istogramma │ │ Trend Cpk ultimi 30 giorni │ │ +│ │ [Plotly.js] │ │ [Plotly.js] │ │ +│ │ ▄ │ │ · · · │ │ +│ │ ▄█▄ │ │ · · · · │ │ +│ │ ▄███▄ │ │ · · · │ │ +│ │▄█████▄ │ │ │ │ +│ └────────────────────┘ └────────────────────────────────┘ │ +│ │ +│ [Scarica Report PDF] [Confronta Versioni] │ +└──────────────────────────────────────────────────────────────┘ +``` + +--- + +## 9. Fasi di Sviluppo + +| Fase | Descrizione | Dipendenze | +|------|-------------|------------| +| **FASE 0** | Setup Progetto: repo, dipendenze, config, DB | Nessuna | +| **FASE 1** | Backend Core: modelli, auth, API ricette/utenti/files | Fase 0 | +| **FASE 2** | Client Base: layout, login, navbar ruoli, tema, lingua | Fase 0 (parallelo a Fase 1) | +| **FASE 3** | Flusso MeasurementTec: ricette, task, misure, numpad, CSV | Fase 1 + 2 | +| **FASE 4** | Editor Maker: Fabric.js, upload, task/subtask, versioning | Fase 1 + 2 | +| **FASE 5** | Calibro USB + Barcode: Web Serial, html5-qrcode | Fase 3 | +| **FASE 6** | Dashboard Metrologist: SPC, grafici Plotly, report PDF | Fase 1 + 2 | +| **FASE 7** | Polish & Testing: i18n, test E2E, security, docs | Tutte | + +**Note**: Fase 3, 4 e 6 possono essere sviluppate in **parallelo** dopo che Fase 1 e 2 sono complete. + +--- + +## 10. Dettaglio Task e Agenti + +> **Legenda Agenti** (prefisso `oh-my-claudecode:` quando usati via Task tool): +> +> | Agente | Modello | Specializzazione | +> |--------|---------|-----------------| +> | `executor-low` | Haiku | Task semplici, file singolo | +> | `executor` | Sonnet | Implementazione standard | +> | `executor-high` | Opus | Logica complessa, multi-file | +> | `designer` | Sonnet | UI/UX, template HTML, CSS | +> | `designer-high` | Opus | Design system complesso | +> | `architect` | Opus | Analisi, verifica architettura | +> | `researcher` | Sonnet | Ricerca documentazione, API docs | +> | `tdd-guide` | Sonnet | Test-driven development | +> | `build-fixer` | Sonnet | Fix errori build/type | +> | `writer` | Haiku | Documentazione, traduzioni | +> | `code-reviewer` | Opus | Review qualita codice | +> | `security-reviewer` | Opus | Review sicurezza | + +--- + +### FASE 0 - Setup Progetto + +| # | Task | Subtask | Agente | Parallelo | +|---|------|---------|--------|-----------| +| 0.1 | **Init repository** | | | | +| | | 0.1.1 - Creare .gitignore (Python, Node, uploads) | `executor-low` | | +| | | 0.1.2 - Creare .env.example (DB, API keys, paths) | `executor-low` | Con 0.1.1 | +| | | 0.1.3 - Creare CLAUDE.md progetto | `writer` | Con 0.1.1 | +| 0.2 | **Setup server** | | | | +| | | 0.2.1 - Creare requirements.txt server | `executor-low` | | +| | | 0.2.2 - Creare main.py FastAPI base con CORS | `executor` | Dopo 0.2.1 | +| | | 0.2.3 - Creare config.py (settings da .env) | `executor` | Con 0.2.2 | +| | | 0.2.4 - Creare database.py (async engine + session) | `executor` | Dopo 0.2.2 | +| 0.3 | **Setup client** | | | **Parallelo con 0.2** | +| | | 0.3.1 - Creare requirements.txt client | `executor-low` | | +| | | 0.3.2 - Creare app.py Flask base con blueprints | `executor` | Dopo 0.3.1 | +| | | 0.3.3 - Setup TailwindCSS (config + build) | `executor` | Con 0.3.2 | +| | | 0.3.4 - Creare struttura static/ + vendor/ | `executor-low` | Con 0.3.2 | +| 0.4 | **Database** | | | | +| | | 0.4.1 - Setup Alembic migrations | `executor` | Dopo 0.2.4 | +| | | 0.4.2 - docker-compose.yml con MySQL 8.0 | `executor-low` | Con 0.4.1 | + +--- + +### FASE 1 - Backend Core + +| # | Task | Subtask | Agente | Parallelo | +|---|------|---------|--------|-----------| +| 1.1 | **Modelli SQLAlchemy** | | | | +| | | 1.1.1 - models/user.py (User + roles JSON + is_admin) | `executor` | | +| | | 1.1.2 - models/recipe.py (Recipe + RecipeVersion) | `executor` | Con 1.1.1 | +| | | 1.1.3 - models/task.py (RecipeTask + RecipeSubtask) | `executor` | Con 1.1.1 | +| | | 1.1.4 - models/measurement.py (Measurement) | `executor` | Con 1.1.1 | +| | | 1.1.5 - models/access_log.py + setting.py | `executor-low` | Con 1.1.1 | +| 1.2 | **Schemas Pydantic** | | | **Parallelo con 1.1** | +| | | 1.2.1 - schemas/user.py (Create, Update, Response) | `executor` | | +| | | 1.2.2 - schemas/recipe.py (+ version schemas) | `executor` | Con 1.2.1 | +| | | 1.2.3 - schemas/task.py (+ subtask schemas) | `executor` | Con 1.2.1 | +| | | 1.2.4 - schemas/measurement.py (Create, Query, Response) | `executor` | Con 1.2.1 | +| | | 1.2.5 - schemas/statistics.py (SPC response schemas) | `executor` | Con 1.2.1 | +| 1.3 | **Middleware** | | | **Parallelo con 1.1** | +| | | 1.3.1 - middleware/api_key.py (verifica header) | `executor` | | +| | | 1.3.2 - middleware/logging.py (access log automatico) | `executor-low` | Con 1.3.1 | +| 1.4 | **Router Auth** | | | Dopo 1.1, 1.2 | +| | | 1.4.1 - routers/auth.py (login, logout, me) | `executor` | | +| | | 1.4.2 - services/auth_service.py (hash, verify, API key) | `executor` | Con 1.4.1 | +| 1.5 | **Router Users** | | | Con 1.4 | +| | | 1.5.1 - routers/users.py (CRUD + admin check) | `executor` | | +| 1.6 | **Router Recipes** | | | Dopo 1.4 | +| | | 1.6.1 - routers/recipes.py (CRUD + versioning endpoints) | `executor-high` | | +| | | 1.6.2 - services/recipe_service.py (copy-on-write logic) | `executor-high` | Con 1.6.1 | +| 1.7 | **Router Tasks** | | | Con 1.6 | +| | | 1.7.1 - routers/tasks.py (CRUD task + subtask + reorder) | `executor` | | +| 1.8 | **Router Files** | | | Con 1.6 | +| | | 1.8.1 - routers/files.py (upload + download + thumbnail) | `executor` | | +| | | 1.8.2 - Configurazione storage disco (paths, limits) | `executor-low` | Con 1.8.1 | +| 1.9 | **Router Settings** | | | Con 1.4 | +| | | 1.9.1 - routers/settings.py (GET/PUT + logo upload) | `executor` | | +| 1.10 | **Test Backend** | | | Dopo 1.4-1.9 | +| | | 1.10.1 - conftest.py + test_auth.py | `tdd-guide` | | +| | | 1.10.2 - test_recipes.py (CRUD + versioning + measurement count) | `tdd-guide` | Con 1.10.1 | +| | | 1.10.3 - test_measurements.py (save + query + pass/fail) | `tdd-guide` | Con 1.10.1 | +| 1.11 | **Review Backend** | | | Dopo 1.10 | +| | | 1.11.1 - Code review completa | `code-reviewer` | | +| | | 1.11.2 - Security review (API key, SQL injection, upload) | `security-reviewer` | Con 1.11.1 | + +--- + +### FASE 2 - Client Base + +| # | Task | Subtask | Agente | Parallelo | +|---|------|---------|--------|-----------| +| 2.1 | **Layout e Tema** | | | | +| | | 2.1.1 - base.html (layout, navbar, footer, logo, brand) | `designer` | | +| | | 2.1.2 - themes.css (CSS variables: brand colors + dark/light) | `designer` | Con 2.1.1 | +| | | 2.1.3 - Alpine.js init + tema toggle + localStorage | `designer` | Con 2.1.1 | +| 2.2 | **Login e Profilo** | | | Dopo 2.1 | +| | | 2.2.1 - blueprints/auth.py (routes Flask) | `executor` | | +| | | 2.2.2 - login.html (logo azienda, form, lingua/tema) | `designer` | Con 2.2.1 | +| | | 2.2.3 - profile.html (modifica: nome, lingua, tema) | `designer` | Con 2.2.1 | +| 2.3 | **API Client** | | | **Parallelo con 2.1** | +| | | 2.3.1 - services/api_client.py (wrapper requests + API key) | `executor` | | +| 2.4 | **Navbar dinamica** | | | Dopo 2.1, 2.2 | +| | | 2.4.1 - navbar.html (voci basate su ruoli utente) | `designer` | | +| | | 2.4.2 - Logo + nome azienda caricati da settings API | `executor` | Con 2.4.1 | +| 2.5 | **i18n Setup** | | | **Parallelo con 2.1** | +| | | 2.5.1 - Configurare Flask-Babel + babel.cfg | `executor` | | +| | | 2.5.2 - Configurare alpinejs-i18n per switch dinamico | `executor` | Con 2.5.1 | +| | | 2.5.3 - Stringhe base IT/EN (login, navbar, common) | `writer` | Dopo 2.5.1 | + +--- + +### FASE 3 - Flusso MeasurementTec + +| # | Task | Subtask | Agente | Parallelo | +|---|------|---------|--------|-----------| +| 3.1 | **Selezione Ricetta** | | | | +| | | 3.1.1 - blueprints/measure.py (routes) | `executor` | | +| | | 3.1.2 - select_recipe.html (lista + ricerca + lotto/seriale input) | `designer` | Con 3.1.1 | +| | | 3.1.3 - Supporto parametro URL (?recipe=CODE&lot=X&serial=Y) | `executor` | Dopo 3.1.2 | +| 3.2 | **Visualizzazione Task** | | | Dopo 3.1 | +| | | 3.2.1 - task_list.html (lista task con miniature + progress bar) | `designer` | | +| | | 3.2.2 - PDF.js viewer integrato per file PDF | `executor` | Con 3.2.1 | +| | | 3.2.3 - Canvas overlay per annotazioni (display-only) | `executor` | Dopo 3.2.2 | +| | | 3.2.4 - annotation-viewer.js (render Fabric.js JSON su canvas) | `executor` | Con 3.2.3 | +| 3.3 | **Esecuzione Subtask** | | | Dopo 3.2 | +| | | 3.3.1 - task_execute.html (split: immagine sx, misure dx) | `designer` | | +| | | 3.3.2 - measurement_feedback.html (barra colore pass/warn/fail) | `designer` | Con 3.3.1 | +| | | 3.3.3 - next_measurement.html (indicatore prossima misura) | `designer` | Con 3.3.1 | +| | | 3.3.4 - Salvataggio misure via API + calcolo pass/fail client | `executor` | Dopo 3.3.1 | +| 3.4 | **Tastierino Touch (Numpad)** | | | **Parallelo con 3.3** | +| | | 3.4.1 - numpad.html (componente Alpine.js) | `designer` | | +| | | 3.4.2 - numpad.js (cifre, punto, +/-, backspace, clear, conferma) | `designer` | Con 3.4.1 | +| | | 3.4.3 - numpad.css (bottoni 56x56px, touch-friendly) | `designer` | Con 3.4.1 | +| | | 3.4.4 - Integrazione numpad nel form task_execute | `executor` | Dopo 3.4.2 | +| 3.5 | **Export CSV** | | | Dopo 3.3 | +| | | 3.5.1 - csv-export.js (Blob API + config delimiter/decimal) | `executor` | | +| | | 3.5.2 - Bottone download CSV in riepilogo | `designer` | Con 3.5.1 | +| 3.6 | **Completamento Task** | | | Dopo 3.3 | +| | | 3.6.1 - task_complete.html (riepilogo: tutte misure + pass/fail + CSV) | `designer` | | +| 3.7 | **Router Measurements API** | | | **Parallelo con 3.1** | +| | | 3.7.1 - routers/measurements.py (POST singola, POST batch, GET query) | `executor` | | +| | | 3.7.2 - services/measurement_service.py (pass/fail calc, deviation) | `executor` | Con 3.7.1 | + +--- + +### FASE 4 - Editor Maker + +| # | Task | Subtask | Agente | Parallelo | +|---|------|---------|--------|-----------| +| 4.1 | **Lista Ricette** | | | | +| | | 4.1.1 - blueprints/maker.py (routes) | `executor` | | +| | | 4.1.2 - recipe_list.html (CRUD, filtri, badge versione, stato) | `designer` | Con 4.1.1 | +| 4.2 | **Editor Ricetta** | | | Dopo 4.1 | +| | | 4.2.1 - recipe_editor.html (form metadati + upload area) | `designer` | | +| | | 4.2.2 - Upload immagine/PDF con anteprima live | `executor` | Con 4.2.1 | +| | | 4.2.3 - Ricerca Context7 docs Fabric.js per annotazioni | `researcher` | **Prima di 4.2.4** | +| | | 4.2.4 - annotation-editor.js (Fabric.js: canvas, oggetti, eventi) | `executor-high` | Dopo 4.2.3 | +| | | 4.2.5 - Toolbar: frecce, marker numerati, rettangoli area, elimina | `designer-high` | Con 4.2.4 | +| | | 4.2.6 - Export/import annotazioni JSON (save/load Fabric.js canvas) | `executor` | Dopo 4.2.4 | +| 4.3 | **Editor Task/Subtask** | | | Dopo 4.2 | +| | | 4.3.1 - task_editor.html (CRUD task inline, drag reorder) | `designer` | | +| | | 4.3.2 - Form subtask: marker#, descrizione, tipo, nominale, UTL/UWL/LWL/LTL, unita | `designer` | Con 4.3.1 | +| | | 4.3.3 - Collegamento bidirezionale marker Fabric.js ↔ subtask list | `executor-high` | Dopo 4.3.2 | +| 4.4 | **Anteprima Ricetta** | | | Dopo 4.2, 4.3 | +| | | 4.4.1 - recipe_preview.html (esattamente come vista MeasurementTec) | `designer` | | +| 4.5 | **Versioning UI** | | | Dopo 4.2 | +| | | 4.5.1 - Avviso "N misure esistenti" su modifica + conferma | `executor` | | +| | | 4.5.2 - version_history.html (storico versioni + change notes) | `designer` | Con 4.5.1 | + +--- + +### FASE 5 - Calibro USB + Barcode + +| # | Task | Subtask | Agente | Parallelo | +|---|------|---------|--------|-----------| +| 5.1 | **Web Serial API (Calibro USB)** | | | | +| | | 5.1.1 - Ricerca Context7 docs Web Serial API | `researcher` | | +| | | 5.1.2 - caliper.js (requestPort, open, read, parse protocollo) | `executor` | Dopo 5.1.1 | +| | | 5.1.3 - caliper_status.html (icona: disconnesso/connesso/errore) | `designer` | Con 5.1.2 | +| | | 5.1.4 - Integrazione: dato calibro → campo numpad automatico | `executor` | Dopo 5.1.2 | +| 5.2 | **Barcode Scanner** | | | **Parallelo con 5.1** | +| | | 5.2.1 - barcode.js (html5-qrcode: init camera, decode callback) | `executor` | | +| | | 5.2.2 - barcode_scanner.html (modal con preview camera) | `designer` | Con 5.2.1 | +| | | 5.2.3 - Integrazione: barcode decodificato → API /recipes/code/{code} | `executor` | Dopo 5.2.1 | + +--- + +### FASE 6 - Dashboard Metrologist + +| # | Task | Subtask | Agente | Parallelo | +|---|------|---------|--------|-----------| +| 6.1 | **Backend SPC** | | | | +| | | 6.1.1 - Ricerca librerie SPC Python (spc-plotly, scipy.stats) | `researcher` | | +| | | 6.1.2 - services/spc_service.py (Cp, Cpk, Pp, Ppk, X-bar, R, sigma) | `executor-high` | Dopo 6.1.1 | +| | | 6.1.3 - routers/statistics.py (tutti gli endpoint con filtri) | `executor` | Con 6.1.2 | +| | | 6.1.4 - test_spc.py (test calcoli con dataset noti) | `tdd-guide` | Dopo 6.1.2 | +| 6.2 | **Frontend Dashboard** | | | Dopo 6.1 | +| | | 6.2.1 - blueprints/statistics.py (routes Flask) | `executor` | | +| | | 6.2.2 - dashboard.html (overview: riepilogo + capability + alert) | `designer` | Con 6.2.1 | +| | | 6.2.3 - filters.html (componente filtri riusabile con htmx) | `designer` | Con 6.2.1 | +| 6.3 | **Grafici SPC Plotly.js** | | | Dopo 6.2 | +| | | 6.3.1 - spc-charts.js (configurazioni Plotly: layout, colori, zone) | `executor` | | +| | | 6.3.2 - control_chart.html (X-bar / R con zone colorate) | `designer` | Con 6.3.1 | +| | | 6.3.3 - histogram.html (distribuzione + curva normale sovrapposta) | `designer` | Con 6.3.1 | +| | | 6.3.4 - capability.html (gauge Cp/Cpk/Pp/Ppk con indicatori) | `designer` | Con 6.3.1 | +| | | 6.3.5 - trend.html (trend temporali + confronto periodi + versioni) | `designer` | Con 6.3.1 | +| 6.4 | **Alert e Regole** | | | Dopo 6.3 | +| | | 6.4.1 - Western Electric rules detection in spc_service.py | `executor-high` | | +| | | 6.4.2 - Visualizzazione alert su dashboard (badge + dettaglio) | `designer` | Dopo 6.4.1 | +| 6.5 | **Report PDF** | | | Dopo 6.3 | +| | | 6.5.1 - services/report_service.py (WeasyPrint + Kaleido) | `executor` | | +| | | 6.5.2 - base_report.html (layout report con logo azienda + brand) | `designer` | Con 6.5.1 | +| | | 6.5.3 - spc_report.html (grafici SVG + tabelle statistiche) | `designer` | Con 6.5.1 | +| | | 6.5.4 - measurement_report.html (tabella misure dettagliata) | `designer` | Con 6.5.1 | +| | | 6.5.5 - routers/reports.py (genera + serve PDF download) | `executor` | Dopo 6.5.1 | + +--- + +### FASE 7 - Polish & Testing + +| # | Task | Subtask | Agente | Parallelo | +|---|------|---------|--------|-----------| +| 7.1 | **i18n Completo** | | | | +| | | 7.1.1 - Estrarre tutte le stringhe traducibili (pybabel extract) | `executor` | | +| | | 7.1.2 - Traduzione IT completa (messages.po) | `writer` | Dopo 7.1.1 | +| | | 7.1.3 - Traduzione EN completa (messages.po) | `writer` | Con 7.1.2 | +| | | 7.1.4 - Stringhe client-side Alpine.js (JSON IT/EN) | `writer` | Con 7.1.2 | +| 7.2 | **Ottimizzazione Tablet** | | | **Parallelo con 7.1** | +| | | 7.2.1 - Test responsive su risoluzioni tablet Windows | `designer` | | +| | | 7.2.2 - Touch target minimi 44x44px su tutti i bottoni | `designer` | Con 7.2.1 | +| | | 7.2.3 - Performance: lazy load PDF.js, ottimizzare bundle JS | `executor` | Con 7.2.1 | +| 7.3 | **Testing End-to-End** | | | Dopo 7.1 | +| | | 7.3.1 - Test flusso MeasurementTec completo | `tdd-guide` | | +| | | 7.3.2 - Test flusso Maker completo (incluso versioning) | `tdd-guide` | Con 7.3.1 | +| | | 7.3.3 - Test flusso Metrologist completo (SPC + PDF) | `tdd-guide` | Con 7.3.1 | +| 7.4 | **Security & Review finale** | | | Dopo 7.3 | +| | | 7.4.1 - Security review completa (OWASP top 10) | `security-reviewer` | | +| | | 7.4.2 - Code review finale | `code-reviewer` | Con 7.4.1 | +| | | 7.4.3 - Verifica architettura con architect | `architect` | Con 7.4.1 | +| 7.5 | **Documentazione** | | | **Parallelo con 7.3** | +| | | 7.5.1 - API.md (documentazione API REST con esempi) | `writer` | | +| | | 7.5.2 - DEPLOYMENT.md (guida: server, client, MySQL, ZeroTier) | `writer` | Con 7.5.1 | +| | | 7.5.3 - USER_GUIDE.md (guida per MeasurementTec, Maker, Metrologist) | `writer` | Con 7.5.1 | + +--- + +## 11. Versioning Ricette + +### Principio: Immutable Copy-on-Write + +**Regola fondamentale**: le versioni delle ricette sono **IMMUTABILI**. Una volta creata, una versione non viene mai modificata. Ogni modifica crea una nuova versione. + +### Flusso di Modifica Ricetta + +``` +1. Maker apre ricetta "COUPLING-256" (versione corrente: v3) +2. Maker clicca "Modifica" +3. Sistema chiama GET /api/recipes/{id}/versions/3/measurement-count +4. Risultato: + + ├── SE measurement_count > 0: + │ └── Mostra avviso: + │ ╔══════════════════════════════════════════════════╗ + │ ║ Attenzione: Esistono 847 misure per la v3. ║ + │ ║ La modifica creera la versione v4. ║ + │ ║ Le misure esistenti resteranno legate a v3. ║ + │ ║ ║ + │ ║ Motivo modifica: [________________________] ║ + │ ║ ║ + │ ║ [Procedi] [Annulla] ║ + │ ╚══════════════════════════════════════════════════╝ + │ + └── SE measurement_count == 0: + └── Procede alla modifica (crea comunque nuova versione per audit) + +5. Maker effettua modifiche (task, subtask, annotazioni, tolleranze) +6. Maker salva → Server: + a. Crea v4 copiando struttura da v3 + b. Applica le modifiche del Maker + c. Copia file immagini/PDF nella cartella v4 + d. UPDATE recipe_versions SET is_current = FALSE WHERE version_number = 3 + e. INSERT recipe_versions (version_number = 4, is_current = TRUE) + f. INSERT recipe_version_audit (change_type = 'UPDATE', change_reason = ...) +7. MeasurementTec successivi lavoreranno con v4 +8. Misure storiche restano legate a v3 (immutabile, intoccabile) +``` + +### Query Metrologist Cross-Versione + +Il Metrologist puo: +- Filtrare misure per **versione specifica** della ricetta +- Vedere **tutte le versioni aggregate** (timeline completa) +- **Confrontare statistiche tra versioni** (es: "la v4 ha migliorato il Cpk?") +- Vedere quando una versione e stata attivata e perche (audit trail) + +--- + +## 12. Gestione File Server + +### Struttura Storage + +``` +server/uploads/ # Root (configurabile via UPLOAD_DIR env var) +├── images/ # Immagini task +│ └── {recipe_id}/ +│ └── {version_id}/ +│ ├── task_1_drawing.jpg # Originale +│ └── task_1_drawing_thumb.jpg # Miniatura 200px auto-generata +│ +├── pdfs/ # PDF task (single page consigliato) +│ └── {recipe_id}/ +│ └── {version_id}/ +│ └── task_2_technical.pdf +│ +├── logos/ # Logo azienda configurabile +│ └── company_logo.png # Caricato da Admin +│ +└── reports/ # Report PDF generati (temporanei) + └── spc_report_20260206_a1b2c3.pdf # Auto-pulizia dopo 24h +``` + +### Regole Storage + +| Regola | Dettaglio | +|--------|-----------| +| **Dimensione max upload** | Configurabile via `system_settings` (default 50MB) | +| **Formati immagine** | JPG, JPEG, PNG, WebP, BMP | +| **Formati PDF** | Solo PDF | +| **Miniature** | Auto-generate a 200px con Pillow per lista task | +| **Naming** | `task_{order}_{original_name}.{ext}` (sanitizzato) | +| **Versioning file** | File copiati fisicamente quando si crea nuova versione ricetta | +| **Pulizia report** | Cron/scheduled task elimina report > 24h | +| **Backup** | Directory `uploads/` inclusa nei backup (escluso `reports/`) | +| **Spazio disco** | Monitorare con health check endpoint | + +### Configurazione + +```python +# server/config.py +UPLOAD_DIR = os.environ.get("UPLOAD_DIR", "./uploads") +MAX_UPLOAD_SIZE_MB = int(os.environ.get("MAX_UPLOAD_SIZE_MB", "50")) +ALLOWED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"} +ALLOWED_PDF_EXTENSIONS = {".pdf"} +THUMBNAIL_SIZE = (200, 200) +REPORT_RETENTION_HOURS = 24 +``` + +--- + +## 13. Tastierino Touch (Numpad) + +### Design Visuale + +``` +┌───────────────────────────────────────────┐ +│ Marker #2 - Diametro Interno (mm) │ +│ Nominale: 12.500 Tolleranza: ±0.050 │ +│ │ +│ ┌─────────────────────────────────┐ │ +│ │ 12.487 │ │ +│ └─────────────────────────────────┘ │ +│ ████████████████████████████░░░░░░ PASS │ +│ │ +│ ┌──────┬──────┬──────┬──────┐ │ +│ │ │ │ │ │ │ +│ │ 7 │ 8 │ 9 │ ⌫ │ │ +│ │ │ │ │ │ │ +│ ├──────┼──────┼──────┼──────┤ │ +│ │ │ │ │ │ │ +│ │ 4 │ 5 │ 6 │ C │ │ +│ │ │ │ │ │ │ +│ ├──────┼──────┼──────┼──────┤ │ +│ │ │ │ │ │ │ +│ │ 1 │ 2 │ 3 │ +/- │ │ +│ │ │ │ │ │ │ +│ ├──────┼──────┼──────┼──────┤ │ +│ │ │ │ │ │ │ +│ │ 0 │ . │ │ ✓ │ │ +│ │ │ │ │ (OK) │ │ +│ └──────┴──────┴──────┴──────┘ │ +│ │ +│ → Prossima: Marker #3 - Lunghezza totale │ +└───────────────────────────────────────────┘ +``` + +### Specifiche + +| Caratteristica | Dettaglio | +|----------------|-----------| +| **Dimensione bottoni** | Minimo 56x56px per touch ottimale su tablet | +| **Layout** | 4x4 grid: cifre 0-9, punto `.`, backspace `⌫`, clear `C`, +/- , conferma `✓` | +| **Font display** | JetBrains Mono, dimensione grande (24px+), ben leggibile | +| **Feedback colore** | Bordo campo cambia in tempo reale: verde/giallo/rosso | +| **Barra feedback** | Progress bar colorata sotto il display con stato PASS/WARNING/FAIL | +| **Validazione** | Impedisce doppio punto, max 6 decimali, range ragionevole | +| **Auto-focus** | Numpad si attiva sul subtask corrente automaticamente | +| **Tasto Enter HW** | Tasto Enter da tastiera fisica = bottone conferma ✓ | +| **USB override** | Dato da calibro USB popola il campo e mostra feedback automaticamente | +| **Navigazione** | Dopo conferma, avanza automaticamente al subtask successivo | +| **Vibrazione** | `navigator.vibrate(50)` su pressione tasto (se supportato) | + +--- + +## 14. Internazionalizzazione e Temi + +### i18n: Italiano / Inglese + +**Approccio ibrido**: + +| Layer | Tecnologia | Stringhe | +|-------|-----------|----------| +| **Server (Jinja2)** | Flask-Babel | Template HTML, messaggi errore, label form | +| **Client (Alpine.js)** | alpinejs-i18n | Stringhe dinamiche, numpad, feedback real-time | +| **Report PDF** | Flask-Babel | Contenuto report generato server-side | + +**Workflow traduzione**: +1. Sviluppatore marca stringhe: `{{ _('Dashboard') }}` o `{% trans %}Misure{% endtrans %}` +2. `pybabel extract` → genera `messages.pot` +3. `pybabel update` → aggiorna `it/messages.po` e `en/messages.po` +4. Traduttore compila → `pybabel compile` → file `.mo` + +**Preferenza utente**: salvata in `users.language_pref`, applicata su ogni request. + +### Tema Dark / Light + +**Implementazione CSS Variables**: + +```css +/* themes.css */ +:root { + /* Brand colors (sempre uguali) */ + --color-primary: #2563EB; + --color-secondary: #64748B; + --color-accent: #1E40AF; + --color-pass: #059669; + --color-warning: #D97706; + --color-fail: #DC2626; + + /* Light theme (default) */ + --bg-primary: #F8FAFC; + --bg-secondary: #F1F5F9; + --bg-card: #FFFFFF; + --text-primary: #0F172A; + --text-secondary: #475569; + --border-color: #E2E8F0; +} + +.dark { + /* Dark theme */ + --bg-primary: #0F172A; + --bg-secondary: #1E293B; + --bg-card: #334155; + --text-primary: #F1F5F9; + --text-secondary: #94A3B8; + --border-color: #475569; +} +``` + +**Preferenza utente**: salvata in `users.theme_pref` + `localStorage` per applicazione immediata. + +--- + +## 15. Sicurezza e Rete + +### Autenticazione API Key + +| Aspetto | Dettaglio | +|---------|-----------| +| **Header** | `X-API-Key: ` su ogni request | +| **Generazione** | 64 caratteri random (secrets.token_urlsafe) | +| **Storage** | Hash nel DB, chiave in chiaro solo al momento della generazione | +| **Rotazione** | Admin puo rigenerare API key per utente | +| **Rate limiting** | Opzionale (future): limitare request per API key | + +### HTTPS su ZeroTier + +| Ambiente | Certificato | Note | +|----------|-------------|------| +| **Sviluppo** | Self-signed | Generato con openssl, accettare nel browser | +| **Produzione** | Let's Encrypt + Caddy | Caddy reverse proxy con auto-HTTPS | + +### Best Practice Sicurezza + +- Password hashate con bcrypt (cost factor 12) +- SQL injection: prevenuta da SQLAlchemy ORM (query parametrizzate) +- XSS: Jinja2 auto-escaping attivo, CSP headers +- Upload: validazione tipo file, dimensione max, sanitizzazione nome +- CORS: configurato solo per dominio client Flask +- API Key: mai loggata in chiaro, trasmessa solo via HTTPS +- Session Flask: cookie `HttpOnly`, `Secure`, `SameSite` + +--- + +## 16. Evoluzioni Future + +### v2.0 - Misura Digitale via Camera + +``` +Tablet camera → Frame capture → OpenCV/MediaPipe +→ Edge detection → Calibrazione (riferimento noto) +→ Misura calcolata → Validazione operatore (accetta/rifiuta/ripeti) +``` + +- **Tecnologie**: OpenCV.js (browser) o Python+OpenCV (server) +- **Prerequisito gia previsto**: campo `input_method` estendibile (aggiungere `'camera'`) +- **API modulare**: aggiungere endpoint senza modifiche strutturali + +### v2.1 - Rete Neurale Analisi Buono/Scarto + +``` +Immagine pezzo → CNN classificazione (buono/scarto/dubbio) +→ Confidence score: + > 95% → Auto-classificazione + 70-95% → Review operatore + < 70% → Classificazione manuale +``` + +- **Tecnologie**: PyTorch/TensorFlow (training), ONNX Runtime (inferenza server), TensorFlow.js (inferenza tablet) +- **Training**: dataset da misure storiche, transfer learning (ResNet/EfficientNet) +- **Prerequisito gia previsto**: sistema immagini, misure storiche nel DB +- **Nuovo**: tabella `ai_predictions` per audit trail predizioni ML + +### v2.2 - Altre Evoluzioni + +| Feature | Descrizione | +|---------|-------------| +| **Offline mode (PWA)** | Service worker, IndexedDB locale, sync alla riconnessione | +| **Dashboard real-time** | WebSocket per aggiornamento live grafici SPC | +| **Notifiche** | Alert email/push quando Cpk scende sotto soglia configurabile | +| **Integrazione ERP** | API per scambio dati con SAP, Oracle, etc. | +| **Multi-stabilimento** | Supporto multi-sede con dati centralizzati | +| **Calibrazione strumenti** | Gestione scadenze calibrazione calibri, alert scadenza | +| **Export AQDEF** | Formato standard automotive per scambio dati qualita | + +--- + +## 17. Decisioni Architetturali + +| # | Decisione | Motivazione | +|---|-----------|-------------| +| D1 | Monorepo server + client | Deploy semplificato, versioning unificato | +| D2 | FastAPI async + asyncmy | Performance per tablet multipli simultanei | +| D3 | Versioning immutabile ricette (copy-on-write) | Integrita dati storici, compliance QMS/ISO | +| D4 | TailwindCSS + CSS Variables | Personalizzazione tema dark/light, look moderno professionale | +| D5 | Plotly.js (browser) + Kaleido + WeasyPrint (PDF) | Grafici interattivi + report PDF professionali vettoriali | +| D6 | Fabric.js per editor annotazioni | Editor completo drag-and-drop, export JSON, MIT license | +| D7 | Web Serial API + fallback input manuale | Supporto calibri USB con fallback universale | +| D8 | API Key authentication | Semplice, adeguata per rete ZeroTier privata | +| D9 | Ruoli combinabili JSON + flag admin separato | Flessibilita massima senza tabelle join complesse | +| D10 | File organizzati per recipe_id/version_id | Isolamento versioni, backup granulare, pulizia facile | +| D11 | Numpad touch custom (56x56px) | UX ottimale su tablet, nessuna dipendenza tastiera fisica | +| D12 | Branding configurabile (logo + nome da DB) | Riusabilita del sistema per clienti/installazioni diverse | +| D13 | i18n ibrido Flask-Babel + alpinejs-i18n | Copertura completa IT/EN: server-side + client-side | +| D14 | Font Inter + JetBrains Mono | UI professionale + numeri allineati e leggibili | +| D15 | Nome "TieMeasureFlow" by Tielogic | Brand diretto: collega (Tie) misure (Measure) in un flusso (Flow) | + +--- + +> **Prossimo passo**: Approvazione piano → Inizio implementazione FASE 0 diff --git a/client/app.py b/client/app.py new file mode 100644 index 0000000..570e4ab --- /dev/null +++ b/client/app.py @@ -0,0 +1,66 @@ +"""TieMeasureFlow Client - Flask Entry Point.""" +import os + +from flask import Flask, redirect, url_for, session, request +from flask_babel import Babel + +from config import Config + + +def get_locale(): + """Get user's preferred language from session or Accept-Language header.""" + # 1. User preference in session + if "language" in session: + return session["language"] + # 2. Browser Accept-Language + return request.accept_languages.best_match( + Config.LANGUAGES.keys(), default="it" + ) + + +def create_app() -> Flask: + """Application factory.""" + app = Flask(__name__) + app.config.from_object(Config) + + # Initialize Flask-Babel + Babel(app, locale_selector=get_locale) + + # Register blueprints + from blueprints.auth import auth_bp + from blueprints.measure import measure_bp + from blueprints.maker import maker_bp + from blueprints.statistics import statistics_bp + + app.register_blueprint(auth_bp) + app.register_blueprint(measure_bp, url_prefix="/measure") + app.register_blueprint(maker_bp, url_prefix="/maker") + app.register_blueprint(statistics_bp, url_prefix="/statistics") + + @app.route("/") + def index(): + """Root redirect to login or dashboard based on session.""" + if "user" in session: + return redirect(url_for("measure.select_recipe")) + return redirect(url_for("auth.login")) + + @app.context_processor + def inject_globals(): + """Inject global variables into all templates.""" + return { + "current_user": session.get("user"), + "current_theme": session.get("theme", "light"), + "current_language": get_locale(), + "languages": Config.LANGUAGES, + } + + return app + + +if __name__ == "__main__": + app = create_app() + app.run( + host=os.getenv("CLIENT_HOST", "0.0.0.0"), + port=int(os.getenv("CLIENT_PORT", "5000")), + debug=True, + ) diff --git a/client/blueprints/__init__.py b/client/blueprints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/client/blueprints/auth.py b/client/blueprints/auth.py new file mode 100644 index 0000000..3374092 --- /dev/null +++ b/client/blueprints/auth.py @@ -0,0 +1,31 @@ +"""Authentication blueprint - login, logout, profile.""" +from flask import Blueprint, render_template, request, redirect, url_for, session, flash + +auth_bp = Blueprint("auth", __name__) + + +@auth_bp.route("/login", methods=["GET", "POST"]) +def login(): + """Login page.""" + if request.method == "POST": + # TODO: Implement API call to server /api/auth/login + pass + return render_template("auth/login.html") + + +@auth_bp.route("/logout") +def logout(): + """Logout - clear session.""" + session.clear() + return redirect(url_for("auth.login")) + + +@auth_bp.route("/profile", methods=["GET", "POST"]) +def profile(): + """User profile - change display name, language, theme.""" + if "user" not in session: + return redirect(url_for("auth.login")) + if request.method == "POST": + # TODO: Implement API call to server /api/auth/me PUT + pass + return render_template("auth/profile.html") diff --git a/client/blueprints/maker.py b/client/blueprints/maker.py new file mode 100644 index 0000000..a9d3b93 --- /dev/null +++ b/client/blueprints/maker.py @@ -0,0 +1,45 @@ +"""Maker blueprint - recipe creation and editing.""" +from flask import Blueprint, render_template, session, redirect, url_for + +maker_bp = Blueprint("maker", __name__) + + +@maker_bp.route("/recipes") +def recipe_list(): + """List all recipes with filters.""" + if "user" not in session: + return redirect(url_for("auth.login")) + return render_template("maker/recipe_list.html") + + +@maker_bp.route("/recipes/new") +@maker_bp.route("/recipes//edit") +def recipe_editor(recipe_id: int | None = None): + """Recipe editor - create or edit.""" + if "user" not in session: + return redirect(url_for("auth.login")) + return render_template("maker/recipe_editor.html", recipe_id=recipe_id) + + +@maker_bp.route("/recipes//tasks") +def task_editor(recipe_id: int): + """Task/subtask editor with tolerances.""" + if "user" not in session: + return redirect(url_for("auth.login")) + return render_template("maker/task_editor.html", recipe_id=recipe_id) + + +@maker_bp.route("/recipes//preview") +def recipe_preview(recipe_id: int): + """Preview recipe as MeasurementTec would see it.""" + if "user" not in session: + return redirect(url_for("auth.login")) + return render_template("maker/recipe_preview.html", recipe_id=recipe_id) + + +@maker_bp.route("/recipes//versions") +def version_history(recipe_id: int): + """Version history with diff.""" + if "user" not in session: + return redirect(url_for("auth.login")) + return render_template("maker/version_history.html", recipe_id=recipe_id) diff --git a/client/blueprints/measure.py b/client/blueprints/measure.py new file mode 100644 index 0000000..3e20465 --- /dev/null +++ b/client/blueprints/measure.py @@ -0,0 +1,36 @@ +"""MeasurementTec blueprint - recipe selection and measurement execution.""" +from flask import Blueprint, render_template, session, redirect, url_for + +measure_bp = Blueprint("measure", __name__) + + +@measure_bp.route("/select") +def select_recipe(): + """Recipe selection page.""" + if "user" not in session: + return redirect(url_for("auth.login")) + return render_template("measure/select_recipe.html") + + +@measure_bp.route("/tasks/") +def task_list(recipe_id: int): + """Task list for selected recipe.""" + if "user" not in session: + return redirect(url_for("auth.login")) + return render_template("measure/task_list.html", recipe_id=recipe_id) + + +@measure_bp.route("/execute/") +def task_execute(task_id: int): + """Execute measurements for a task.""" + if "user" not in session: + return redirect(url_for("auth.login")) + return render_template("measure/task_execute.html", task_id=task_id) + + +@measure_bp.route("/complete/") +def task_complete(recipe_id: int): + """Task completion summary.""" + if "user" not in session: + return redirect(url_for("auth.login")) + return render_template("measure/task_complete.html", recipe_id=recipe_id) diff --git a/client/blueprints/statistics.py b/client/blueprints/statistics.py new file mode 100644 index 0000000..d00aece --- /dev/null +++ b/client/blueprints/statistics.py @@ -0,0 +1,44 @@ +"""Metrologist blueprint - SPC statistics and dashboards.""" +from flask import Blueprint, render_template, session, redirect, url_for + +statistics_bp = Blueprint("statistics", __name__) + + +@statistics_bp.route("/dashboard") +def dashboard(): + """SPC dashboard overview.""" + if "user" not in session: + return redirect(url_for("auth.login")) + return render_template("statistics/dashboard.html") + + +@statistics_bp.route("/control-chart") +def control_chart(): + """X-bar / R control chart.""" + if "user" not in session: + return redirect(url_for("auth.login")) + return render_template("statistics/control_chart.html") + + +@statistics_bp.route("/histogram") +def histogram(): + """Histogram with normal curve.""" + if "user" not in session: + return redirect(url_for("auth.login")) + return render_template("statistics/histogram.html") + + +@statistics_bp.route("/capability") +def capability(): + """Cp/Cpk/Pp/Ppk capability gauge.""" + if "user" not in session: + return redirect(url_for("auth.login")) + return render_template("statistics/capability.html") + + +@statistics_bp.route("/trend") +def trend(): + """Temporal trends and period comparison.""" + if "user" not in session: + return redirect(url_for("auth.login")) + return render_template("statistics/trend.html") diff --git a/client/config.py b/client/config.py new file mode 100644 index 0000000..3ef0db5 --- /dev/null +++ b/client/config.py @@ -0,0 +1,24 @@ +"""TieMeasureFlow Client Configuration.""" +import os +from dotenv import load_dotenv + +load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env')) + + +class Config: + """Flask client configuration.""" + + # Flask + SECRET_KEY = os.getenv("CLIENT_SECRET_KEY", "change-this-to-another-random-secret-key") + + # API Server connection + API_SERVER_URL = os.getenv("API_SERVER_URL", "http://localhost:8000") + + # Session + SESSION_COOKIE_HTTPONLY = True + SESSION_COOKIE_SAMESITE = "Lax" + + # Babel i18n + BABEL_DEFAULT_LOCALE = "it" + BABEL_DEFAULT_TIMEZONE = "Europe/Rome" + LANGUAGES = {"it": "Italiano", "en": "English"} diff --git a/client/requirements.txt b/client/requirements.txt new file mode 100644 index 0000000..7d41032 --- /dev/null +++ b/client/requirements.txt @@ -0,0 +1,10 @@ +# Flask +flask>=3.0.0 +flask-babel>=4.0.0 + +# HTTP Client (to call FastAPI server) +requests>=2.31.0 +urllib3>=2.0.0 + +# Utilities +python-dotenv>=1.0.0 diff --git a/client/services/__init__.py b/client/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/client/services/api_client.py b/client/services/api_client.py new file mode 100644 index 0000000..a10c965 --- /dev/null +++ b/client/services/api_client.py @@ -0,0 +1,80 @@ +"""API Client - wrapper for HTTP requests to FastAPI server.""" +from typing import Any + +import requests +from flask import session + +from config import Config + + +class APIClient: + """HTTP client for TieMeasureFlow API server.""" + + def __init__(self): + self.base_url = Config.API_SERVER_URL.rstrip("/") + self.timeout = 30 + + @property + def _headers(self) -> dict[str, str]: + """Build request headers with API key from session.""" + headers = {"Content-Type": "application/json"} + api_key = session.get("api_key") + if api_key: + headers["X-API-Key"] = api_key + return headers + + def get(self, endpoint: str, params: dict | None = None) -> dict[str, Any]: + """GET request to API server.""" + response = requests.get( + f"{self.base_url}{endpoint}", + headers=self._headers, + params=params, + timeout=self.timeout, + ) + response.raise_for_status() + return response.json() + + def post(self, endpoint: str, data: dict | None = None, files: dict | None = None) -> dict[str, Any]: + """POST request to API server.""" + if files: + headers = {"X-API-Key": session.get("api_key", "")} + response = requests.post( + f"{self.base_url}{endpoint}", + headers=headers, + data=data, + files=files, + timeout=self.timeout, + ) + else: + response = requests.post( + f"{self.base_url}{endpoint}", + headers=self._headers, + json=data, + timeout=self.timeout, + ) + response.raise_for_status() + return response.json() + + def put(self, endpoint: str, data: dict | None = None) -> dict[str, Any]: + """PUT request to API server.""" + response = requests.put( + f"{self.base_url}{endpoint}", + headers=self._headers, + json=data, + timeout=self.timeout, + ) + response.raise_for_status() + return response.json() + + def delete(self, endpoint: str) -> dict[str, Any]: + """DELETE request to API server.""" + response = requests.delete( + f"{self.base_url}{endpoint}", + headers=self._headers, + timeout=self.timeout, + ) + response.raise_for_status() + return response.json() + + +api_client = APIClient() diff --git a/client/static/css/input.css b/client/static/css/input.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/client/static/css/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/client/static/img/.gitkeep b/client/static/img/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/client/static/js/.gitkeep b/client/static/js/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/client/static/vendor/.gitkeep b/client/static/vendor/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/client/tailwind.config.js b/client/tailwind.config.js new file mode 100644 index 0000000..dc7e00d --- /dev/null +++ b/client/tailwind.config.js @@ -0,0 +1,34 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./templates/**/*.html", + "./static/js/**/*.js", + ], + darkMode: 'class', + theme: { + extend: { + colors: { + primary: { + DEFAULT: '#2563EB', + dark: '#1E40AF', + light: '#3B82F6', + }, + steel: { + DEFAULT: '#64748B', + light: '#94A3B8', + dark: '#475569', + }, + measure: { + pass: '#059669', + warning: '#D97706', + fail: '#DC2626', + }, + }, + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], + mono: ['JetBrains Mono', 'monospace'], + }, + }, + }, + plugins: [], +} diff --git a/client/templates/auth/.gitkeep b/client/templates/auth/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/client/templates/components/.gitkeep b/client/templates/components/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/client/templates/maker/.gitkeep b/client/templates/maker/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/client/templates/measure/.gitkeep b/client/templates/measure/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/client/templates/statistics/.gitkeep b/client/templates/statistics/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/client/tests/.gitkeep b/client/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/client/translations/babel.cfg b/client/translations/babel.cfg new file mode 100644 index 0000000..06537e8 --- /dev/null +++ b/client/translations/babel.cfg @@ -0,0 +1,3 @@ +[python: blueprints/**.py] +[jinja2: templates/**.html] +encoding = utf-8 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..19edda2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3.8' + +services: + mysql: + image: mysql:8.0 + container_name: tmflow-mysql + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: root_password_change_me + MYSQL_DATABASE: tiemeasureflow + MYSQL_USER: tmflow + MYSQL_PASSWORD: change_me_in_production + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + command: > + --default-authentication-plugin=mysql_native_password + --character-set-server=utf8mb4 + --collation-server=utf8mb4_unicode_ci + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + mysql_data: + driver: local diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/config.py b/server/config.py new file mode 100644 index 0000000..7ca2ffa --- /dev/null +++ b/server/config.py @@ -0,0 +1,51 @@ +"""TieMeasureFlow Server Configuration.""" +from pathlib import Path +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + """Application settings loaded from environment variables.""" + + # Database + db_host: str = "localhost" + db_port: int = 3306 + db_name: str = "tiemeasureflow" + db_user: str = "tmflow" + db_password: str = "change_me_in_production" + + # Server + server_host: str = "0.0.0.0" + server_port: int = 8000 + server_secret_key: str = "change-this-to-a-random-secret-key" + server_cors_origins: str = "http://localhost:5000" + + # File Storage + upload_dir: str = "uploads" + max_upload_size_mb: int = 50 + + # SSL (Production) + ssl_certfile: str | None = None + ssl_keyfile: str | None = None + + @property + def database_url(self) -> str: + """Async MySQL connection string.""" + return ( + f"mysql+asyncmy://{self.db_user}:{self.db_password}" + f"@{self.db_host}:{self.db_port}/{self.db_name}" + ) + + @property + def cors_origins(self) -> list[str]: + """Parse CORS origins from comma-separated string.""" + return [origin.strip() for origin in self.server_cors_origins.split(",")] + + @property + def upload_path(self) -> Path: + """Absolute path to upload directory.""" + return Path(__file__).parent / self.upload_dir + + model_config = {"env_file": "../.env", "env_file_encoding": "utf-8"} + + +settings = Settings() diff --git a/server/database.py b/server/database.py new file mode 100644 index 0000000..a36c6e5 --- /dev/null +++ b/server/database.py @@ -0,0 +1,51 @@ +"""Async SQLAlchemy database engine and session management.""" +from collections.abc import AsyncGenerator + +from sqlalchemy.ext.asyncio import ( + AsyncSession, + async_sessionmaker, + create_async_engine, +) +from sqlalchemy.orm import DeclarativeBase + +from config import settings + +# Create async engine +engine = create_async_engine( + settings.database_url, + echo=False, + pool_size=10, + max_overflow=20, + pool_recycle=3600, +) + +# Session factory +async_session_factory = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False, +) + + +class Base(DeclarativeBase): + """Base class for all SQLAlchemy models.""" + pass + + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + """Dependency for FastAPI - yields an async database session.""" + async with async_session_factory() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + finally: + await session.close() + + +async def init_db() -> None: + """Create all tables (dev only - use Alembic in production).""" + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) diff --git a/server/main.py b/server/main.py new file mode 100644 index 0000000..7f7e452 --- /dev/null +++ b/server/main.py @@ -0,0 +1,45 @@ +"""TieMeasureFlow Server - FastAPI Entry Point.""" +from contextlib import asynccontextmanager +from collections.abc import AsyncGenerator + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from config import settings +from database import init_db + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + """Application lifespan: startup and shutdown events.""" + # Startup + # Ensure upload directories exist + for subdir in ["images", "pdfs", "logos", "reports"]: + (settings.upload_path / subdir).mkdir(parents=True, exist_ok=True) + + yield + + # Shutdown (cleanup if needed) + + +app = FastAPI( + title="TieMeasureFlow API", + description="API per gestione task misurazioni con calibro manuale", + version="0.1.0", + lifespan=lifespan, +) + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/api/health") +async def health_check() -> dict: + """Health check endpoint.""" + return {"status": "ok", "service": "TieMeasureFlow API", "version": "0.1.0"} diff --git a/server/middleware/__init__.py b/server/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/migrations/alembic.ini b/server/migrations/alembic.ini new file mode 100644 index 0000000..e493b1d --- /dev/null +++ b/server/migrations/alembic.ini @@ -0,0 +1,36 @@ +[alembic] +script_location = . +sqlalchemy.url = mysql+asyncmy://tmflow:change_me_in_production@localhost:3306/tiemeasureflow + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/server/migrations/env.py b/server/migrations/env.py new file mode 100644 index 0000000..97e944f --- /dev/null +++ b/server/migrations/env.py @@ -0,0 +1,80 @@ +"""Alembic environment configuration for async SQLAlchemy.""" +import asyncio +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import pool +from sqlalchemy.ext.asyncio import create_async_engine + +# Alembic Config object +config = context.config + +# Logging configuration +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# Import all models so Alembic can detect them +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from config import settings +from database import Base + +# Override alembic.ini URL with .env settings (keep in sync) +config.set_main_option("sqlalchemy.url", settings.database_url) + +# Import all models here so they register with Base.metadata +# from models.user import User +# from models.recipe import Recipe, RecipeVersion +# from models.task import RecipeTask, RecipeSubtask +# from models.measurement import Measurement +# from models.access_log import AccessLog +# from models.setting import SystemSetting + +target_metadata = Base.metadata + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode.""" + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection): + """Run migrations with connection.""" + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + """Run migrations in 'online' mode with async engine.""" + connectable = create_async_engine( + config.get_main_option("sqlalchemy.url"), + poolclass=pool.NullPool, + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + await connectable.dispose() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode.""" + asyncio.run(run_async_migrations()) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/server/migrations/script.py.mako b/server/migrations/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/server/migrations/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/server/migrations/versions/.gitkeep b/server/migrations/versions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/models/__init__.py b/server/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..876b473 --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1,28 @@ +# FastAPI + ASGI +fastapi>=0.110.0 +uvicorn[standard]>=0.30.0 + +# Database +sqlalchemy[asyncio]>=2.0.0 +asyncmy>=0.2.0 +alembic>=1.13.0 + +# Validation +pydantic>=2.0.0 +pydantic-settings>=2.0.0 + +# Security +passlib[bcrypt]>=1.7.0 +bcrypt>=4.0.0 + +# File handling +pillow>=10.0.0 +python-multipart>=0.0.6 + +# Reports +plotly>=5.0.0 +kaleido>=0.2.0 +weasyprint>=62.0 + +# Utilities +python-dotenv>=1.0.0 diff --git a/server/routers/__init__.py b/server/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/schemas/__init__.py b/server/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/services/__init__.py b/server/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/templates/reports/.gitkeep b/server/templates/reports/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/tests/.gitkeep b/server/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/uploads/images/.gitkeep b/server/uploads/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/uploads/logos/.gitkeep b/server/uploads/logos/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/uploads/pdfs/.gitkeep b/server/uploads/pdfs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/uploads/reports/.gitkeep b/server/uploads/reports/.gitkeep new file mode 100644 index 0000000..e69de29