# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Panoramica TieMeasureFlow by Tielogic - Sistema di gestione task per misurazioni con calibro manuale. Monorepo con **backend FastAPI** (porta 8000) e **frontend Flask** (porta 5000), orchestrati con Docker Compose + Nginx reverse proxy + MySQL 8.0. ## Layout del repository (V2.0.0) A partire dalla migrazione V2.0.0 (struttura conforme alla spec `python-project-spec-design.md`): ``` TieMeasureFlow/ ├── pyproject.toml # Dipendenze monorepo (uv) ├── uv.lock # Lock file riproducibile ├── .python-version # 3.11 ├── Dockerfile # Backend (uv + uvicorn) ├── Dockerfile.frontend # Frontend (uv + gunicorn + Tailwind + Babel) ├── docker-compose.dev.yml # Dev (Nginx) ├── docker-compose.yml # Prod (Traefik + SSL) ├── nginx/ ├── uploads/ # Volume montato in /app/uploads ├── docs/ └── src/ ├── backend/ │ ├── main.py # Entry FastAPI │ ├── config.py │ ├── database.py │ ├── api/ │ │ ├── routers/ # 11 router REST │ │ └── middleware/ # api_key, rate_limit, security_headers, logging │ ├── models/ │ │ ├── orm/ # SQLAlchemy │ │ └── api/ # Pydantic schemas │ ├── services/ # Logica business │ ├── migrations/ # Alembic │ ├── templates/ # Setup page │ └── tests/ # pytest └── frontend/ └── flask_app/ # Flask + Jinja2 + Alpine.js (deroga vs spec React, ├── app.py # giustificata: tablet UX server-side, USB calipers, ├── config.py # workflow operatore con Fabric.js) ├── blueprints/ ├── services/ ├── templates/ ├── static/ ├── translations/ └── tests/ ``` Dipendenze gestite con **uv** (no `requirements.txt`): - `[project] dependencies` = core condivisi (pydantic, dotenv) - `[project.optional-dependencies] server` = backend FastAPI - `[project.optional-dependencies] client` = frontend Flask - `[project.optional-dependencies] dev` = pytest, httpx, aiosqlite ## Comandi di Sviluppo ### Setup iniziale ```bash cp .env.example .env uv sync --extra server --extra client --extra dev # installa tutto ``` ### Avvio servizi (Docker) ```bash docker compose -f docker-compose.dev.yml up -d # Sviluppo (Nginx, porta 80) docker compose up -d # Produzione (Traefik, SSL) docker compose logs -f server # Log server docker compose logs -f client # Log client docker compose ps # Stato servizi ``` ### Avvio manuale (senza Docker) ```bash # Backend (terminale 1) uv run uvicorn src.backend.main:app --reload --host 0.0.0.0 --port 8000 # Frontend (terminale 2) — gunicorn richiede cwd interna per app:create_app() cd src/frontend/flask_app && uv run --project ../../.. gunicorn --bind 0.0.0.0:5000 app:create_app() # TailwindCSS watch (terminale 3) cd src/frontend/flask_app && npx tailwindcss -i static/css/input.css -o static/css/tailwind.css --watch ``` ### Database & Migrations ```bash # alembic.ini in src/backend/migrations/, serve il flag -c uv run alembic -c src/backend/migrations/alembic.ini upgrade head # Applica migrazioni uv run alembic -c src/backend/migrations/alembic.ini revision --autogenerate -m "descrizione" # Genera uv run alembic -c src/backend/migrations/alembic.ini downgrade -1 # Rollback docker compose exec server uv run alembic -c src/backend/migrations/alembic.ini upgrade head # Via Docker ``` Nota: `env.py` aggiunge la project root a `sys.path`, sovrascrive la URL di alembic.ini con quella da `.env` (`settings.database_url`). `script_location = %(here)s` usa path relativo. ### Test ```bash # Tutti i test (backend + frontend) uv run pytest # Solo backend (SQLite in-memory via aiosqlite, no MySQL richiesto) uv run pytest src/backend/tests/ uv run pytest src/backend/tests/test_auth.py uv run pytest src/backend/tests/test_auth.py::test_login_success uv run pytest --cov src/backend # Solo frontend uv run pytest src/frontend/flask_app/tests/ ``` ### Gestione dipendenze (uv) ```bash uv add # core (entrambi) uv add --optional server # solo backend uv add --optional client # solo frontend uv add --optional dev # solo dev/test uv sync --extra server --extra client --extra dev # reinstalla uv lock # rigenera uv.lock ``` ### i18n (Traduzioni) ```bash # Estrai stringhe cd src/frontend/flask_app && uv run pybabel extract -F babel.cfg -k _ -o translations/messages.pot . # Aggiorna catalogo cd src/frontend/flask_app && uv run pybabel update -i translations/messages.pot -d translations # Compila .po → .mo cd src/frontend/flask_app && uv run pybabel compile -d translations # oppure cd src/frontend/flask_app && uv run python compile_translations.py ``` ### Setup iniziale Dopo primo avvio, aprire `http://localhost/api/setup` (o `:8000/api/setup` senza Nginx) con la SETUP_PASSWORD dal `.env` per: inizializzare DB, creare admin, caricare dati demo. ## Architettura ### Flusso richieste ``` Browser/Tablet → Nginx (:80/443) → Flask Client (:5000) → APIClient → FastAPI Server (:8000) → MySQL ``` Il client Flask è un frontend server-side che comunica col backend via REST API. Ogni richiesta dal client al server include l'header `X-API-Key` per autenticazione. ### Server (FastAPI) — `src/backend/` - **main.py**: entry point, lifespan async (`@asynccontextmanager`), registra middleware e 10 router. Health: `GET /api/health` - **config.py**: `Settings` (pydantic_settings.BaseSettings), legge da `../.env`. Rate limits: login 5/min, general 100/min - **database.py**: SQLAlchemy 2.0 async engine con `AsyncSession`, pool 10+20 overflow, `pool_recycle=3600`, `expire_on_commit=False` - **routers/**: auth, users, recipes, tasks, measurements, files, settings, statistics, reports, setup. Il task router (`tasks.py`) usa URL pattern misti: `/api/recipes/{id}/tasks` (list/create), `/api/tasks/{id}` (get/update/delete), `/api/tasks/reorder`, `/api/tasks/{id}/subtasks`, `/api/subtasks/{id}` - **services/**: recipe_service (versioning copy-on-write), measurement_service (calcolo pass/fail), spc_service (Cp/Cpk/control chart, puro stdlib senza numpy), report_service (WeasyPrint PDF + Kaleido SVG), auth_service (bcrypt + API key) - **middleware/**: Stack order (outermost→innermost): AccessLogMiddleware → CORSMiddleware → SecurityHeadersMiddleware → RateLimitMiddleware. Nota: `add_middleware()` in Starlette wrappa l'app, quindi l'ultimo aggiunto (AccessLog) è il più esterno. Il commento in `main.py` dice "outermost" per RateLimit ma è fuorviante. api_key.py (auth dependency `get_current_user()`), rate_limit.py (sliding window 60s per-IP, in-memory dicts), security_headers.py (CSP con `unsafe-eval` per Plotly.js, HSTS solo con SSL), logging.py (audit trail async su DB, esclude /api/health, /docs, /openapi.json, /redoc) - **models/**: User (include `email`, `language_pref`, `theme_pref`), Recipe (`image_path` per preview), RecipeVersion, RecipeTask, RecipeSubtask (`image_path` per immagine specifica), Measurement (`synced_to_csv`, `input_method`), AccessLog, SystemSetting, RecipeVersionAudit - **schemas/**: Pydantic v2 per validazione I/O API - **migrations/**: Alembic con `alembic.ini` e `env.py` nella directory `src/backend/migrations/` - **tests/**: pytest + pytest-asyncio, SQLite in-memory (`sqlite+aiosqlite://`, StaticPool), WeasyPrint mockato via `sys.modules`, rate limit reset tra test ### Client (Flask) — `src/frontend/flask_app/` - **app.py**: factory pattern `create_app()`, CSRF (`WTF_CSRF_TIME_LIMIT=3600`), Babel i18n (`default_locale="it"`) - **blueprints/**: auth (login/logout/session), maker (editor ricette con Fabric.js), measure (esecuzione misurazioni), statistics (dashboard SPC con Plotly.js), admin (gestione utenti CRUD, cambio password, toggle attivo — solo `is_admin`) - **services/api_client.py**: singleton `APIClient` — wrapper HTTP (get/post/put/delete) con gestione errori normalizzata, timeout 30s, header X-API-Key da session - **templates/**: Jinja2 con `{{ _('string') }}` per i18n. Context processor inietta `current_user`, `current_theme`, `current_language`, `company_logo`, `languages` in tutti i template. Filtro custom `tojson_attr` per JSON sicuro in attributi HTML (escapa `"` → `"`) - **static/js/**: Alpine.js 3.x (CDN), Fabric.js 5.3.1 (CDN) + `fabric-debug.js` (copia locale 5.3.0 per debug), locales JSON (it.json, en.json), alpine-init.js (theme store). Componenti specializzati: `numpad.js` (input numerico touch con rilevamento burst USB HID), `caliper.js` (monitor calibro USB passivo), `barcode.js` (scanner QR/barcode con camera via html5-qrcode), `csv-export.js` (export CSV con locale italiano: `;` separatore, `,` decimale), `spc-charts.js` (grafici SPC con Plotly.js), `annotation-editor.js` / `annotation-viewer.js` (editor/viewer annotazioni Fabric.js) - **translations/**: Flask-Babel, cataloghi .po/.mo per IT/EN. Locale selector: `session["language"]` → Accept-Language → `"it"` - **config.py**: `PERMANENT_SESSION_LIFETIME=28800` (8h), cookie secure in produzione, `BABEL_DEFAULT_TIMEZONE="Europe/Rome"` ## Template Structure (`src/frontend/flask_app/templates/base.html`) Ordine blocchi in `base.html`: ``` → {% block extra_head %} (CSS/meta aggiuntivi) → {% block content %} (contenuto pagina) → {% block extra_js %} (script componenti — PRIMA di Alpine) →
``` `|tojson` dentro `