diff --git a/CLAUDE.md b/CLAUDE.md index 226c873..9b1dfc8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,11 +31,13 @@ cd client && npx tailwindcss -i static/css/input.css -o static/css/tailwind.css ### Database & Migrations ```bash -cd server && alembic upgrade head # Applica migrazioni -cd server && alembic revision --autogenerate -m "descrizione" # Genera migrazione -cd server && alembic downgrade -1 # Rollback ultima -docker compose exec server alembic upgrade head # Via Docker +# alembic.ini è in server/migrations/, serve il flag -c +cd server && alembic -c migrations/alembic.ini upgrade head # Applica migrazioni +cd server && alembic -c migrations/alembic.ini revision --autogenerate -m "descrizione" # Genera migrazione +cd server && alembic -c migrations/alembic.ini downgrade -1 # Rollback ultima +docker compose exec server alembic -c migrations/alembic.ini upgrade head # Via Docker ``` +Nota: `env.py` sovrascrive la URL di alembic.ini con quella da `.env` (`settings.database_url`). `script_location = %(here)s` usa path relativo. ### Test ```bash @@ -76,24 +78,67 @@ Browser/Tablet → Nginx (:80/443) → Flask Client (:5000) → APIClient → Fa 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) — `server/` -- **main.py**: entry point, registra middleware (RateLimit → SecurityHeaders → CORS → AccessLog) e 10 router -- **config.py**: `Settings` Pydantic che legge da `.env` -- **database.py**: SQLAlchemy 2.0 async engine con `AsyncSession`, pool 10+20 overflow -- **routers/**: auth, recipes, tasks, measurements, statistics, reports, files, users, settings, setup +- **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 - **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/**: api_key.py (auth dependency), rate_limit.py (sliding window per-IP), security_headers.py (CSP, HSTS), logging.py (audit trail async su DB) -- **models/**: User, Recipe, RecipeVersion, RecipeTask, RecipeSubtask, Measurement, AccessLog, SystemSetting +- **middleware/**: api_key.py (auth dependency), 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) +- **models/**: User, Recipe, RecipeVersion, RecipeTask, RecipeSubtask, Measurement, AccessLog, SystemSetting, RecipeVersionAudit - **schemas/**: Pydantic v2 per validazione I/O API -- **migrations/**: Alembic con alembic.ini nella directory migrations -- **tests/**: pytest + pytest-asyncio, database SQLite in-memory (aiosqlite), fixture pre-popolate (admin_user, maker_user, etc.), WeasyPrint mockato +- **migrations/**: Alembic con `alembic.ini` e `env.py` nella directory `server/migrations/` +- **tests/**: pytest + pytest-asyncio, SQLite in-memory (`sqlite+aiosqlite://`, StaticPool), WeasyPrint mockato via `sys.modules`, rate limit reset tra test ### Client (Flask) — `client/` -- **app.py**: factory pattern `create_app()`, CSRF protection, Babel i18n +- **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) - **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 -- **static/js/**: Alpine.js components, Fabric.js editor, locales JSON (it.json, en.json) -- **translations/**: Flask-Babel, cataloghi .po/.mo per IT/EN +- **templates/**: Jinja2 con `{{ _('string') }}` per i18n. Context processor inietta `current_user`, `current_theme`, `current_language`, `company_logo` in tutti i template +- **static/js/**: Alpine.js 3.x (CDN), Fabric.js 5.3.1 (CDN), locales JSON (it.json, en.json), alpine-init.js (theme store) +- **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 (`client/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) + → +