chore(v2): restructure monorepo to src/ layout with uv

Aligns the repo with the python-project-spec-design.md template chosen
for V2.0.0. Big move, no logic changes. The 3 pre-existing test
failures (test_recipes::test_update_recipe, test_recipes::
test_recipe_versioning, test_tasks::test_reorder_tasks, plus the
client test_save_measurement_proxy) survive unchanged.

Layout changes
- server/        -> src/backend/
- server/middleware/ -> src/backend/api/middleware/
- server/routers/    -> src/backend/api/routers/
- server/models/     -> src/backend/models/orm/
- server/schemas/    -> src/backend/models/api/
- server/uploads/    -> uploads/ (project root, mounted volume)
- server/tests/      -> src/backend/tests/
- client/            -> src/frontend/flask_app/ (Flask kept; React
  deroga is documented in CLAUDE.md, justified by tablet UX, USB
  caliper/barcode workflow and Fabric.js integration)

Tooling
- pyproject.toml: monorepo with [project] core deps and
  optional-dependencies server / client / dev. Replaces both
  server/requirements.txt and client/requirements.txt.
- uv.lock + .python-version (3.11) committed for reproducible builds.
- Dockerfile (root, backend) and Dockerfile.frontend rewritten to use
  uv sync --frozen --no-dev --extra server|client; legacy Dockerfiles
  preserved as Dockerfile.legacy for reference but excluded from build
  context via .dockerignore.
- docker-compose.dev.yml + docker-compose.yml: build context now ".",
  dockerfile pointing to the root files.

Code adjustments forced by the move
- Every "from config|database|models|schemas|services|routers|middleware
  import ..." rewritten to its src.backend.* equivalent (50+ files
  including indented inline imports inside test bodies).
- src/backend/migrations/env.py: insert project root into sys.path so
  alembic can resolve src.backend.* imports regardless of cwd.
- src/backend/config.py: env_file ../../.env (was ../.env), upload_path
  resolves project root via parents[2].
- src/backend/tests/conftest.py + tests: import ... from src.backend.*
  instead of bare names; old per-directory pytest.ini files removed in
  favor of root pyproject.toml [tool.pytest.ini_options].
- .gitignore: uploads/ at root, src/frontend/flask_app/static/css/
  tailwind.css path; .dockerignore tightened.
- CLAUDE.md: rewrote sections "Layout del repository", "Comandi di
  Sviluppo", "Database & Migrations", "Test", "i18n", and all path
  references throughout the architecture sections.

Verified
- uv lock resolves 77 packages; uv sync --extra server --extra client
  --extra dev installs cleanly.
- uv run pytest: 171 passed, 4 pre-existing failures.
- uv run alembic -c src/backend/migrations/alembic.ini check loads
  config and metadata (errors only on the absent local MySQL).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 12:26:47 +02:00
parent 86df67f2e5
commit 1a0431366f
174 changed files with 2568 additions and 308 deletions
+102 -35
View File
@@ -5,10 +5,65 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Panoramica
TieMeasureFlow by Tielogic - Sistema di gestione task per misurazioni con calibro manuale.
Monorepo con **server FastAPI** (backend API, porta 8000) e **client Flask** (frontend tablet, porta 5000), orchestrati con Docker Compose + Nginx reverse proxy + MySQL 8.0.
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)
@@ -20,51 +75,63 @@ docker compose ps # Stato servizi
### Avvio manuale (senza Docker)
```bash
# Server (terminale 1)
cd server && uvicorn main:app --reload --host 0.0.0.0 --port 8000
# Backend (terminale 1)
uv run uvicorn src.backend.main:app --reload --host 0.0.0.0 --port 8000
# Client (terminale 2)
cd client && flask run --host 0.0.0.0 --port 5000
# 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 client && npx tailwindcss -i static/css/input.css -o static/css/tailwind.css --watch
cd src/frontend/flask_app && npx tailwindcss -i static/css/input.css -o static/css/tailwind.css --watch
```
### Database & Migrations
```bash
# 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
# 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` sovrascrive la URL di alembic.ini con quella da `.env` (`settings.database_url`). `script_location = %(here)s` usa path relativo.
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
# Server (usa SQLite in-memory via aiosqlite, no MySQL richiesto)
cd server && pytest # Tutti i test
cd server && pytest tests/test_auth.py # Singolo modulo
cd server && pytest tests/test_auth.py::test_login_success # Singolo test
cd server && pytest --cov # Con copertura
# Tutti i test (backend + frontend)
uv run pytest
# Client
cd client && pytest
cd client && pytest tests/test_auth.py
# 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 <pacchetto> # core (entrambi)
uv add --optional server <pacchetto> # solo backend
uv add --optional client <pacchetto> # solo frontend
uv add --optional dev <pacchetto> # solo dev/test
uv sync --extra server --extra client --extra dev # reinstalla
uv lock # rigenera uv.lock
```
### i18n (Traduzioni)
```bash
# Estrai stringhe
cd client && pybabel extract -F babel.cfg -k _ -o translations/messages.pot .
cd src/frontend/flask_app && uv run pybabel extract -F babel.cfg -k _ -o translations/messages.pot .
# Aggiorna catalogo
cd client && pybabel update -i translations/messages.pot -d translations
cd src/frontend/flask_app && uv run pybabel update -i translations/messages.pot -d translations
# Compila .po → .mo
cd client && pybabel compile -d translations
cd src/frontend/flask_app && uv run pybabel compile -d translations
# oppure
cd client && python compile_translations.py
cd src/frontend/flask_app && uv run python compile_translations.py
```
### Setup iniziale
@@ -78,7 +145,7 @@ 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/`
### 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`
@@ -87,10 +154,10 @@ Il client Flask è un frontend server-side che comunica col backend via REST API
- **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 `server/migrations/`
- **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) — `client/`
### 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
@@ -99,7 +166,7 @@ Il client Flask è un frontend server-side che comunica col backend via REST API
- **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`)
## Template Structure (`src/frontend/flask_app/templates/base.html`)
Ordine blocchi in `base.html`:
```
@@ -127,7 +194,7 @@ In alternativa, usare il filtro custom `|tojson_attr` (registrato in `app.py`) c
Per selettori CSS in `x-data`: usare `meta[name=csrf-token]` senza virgolette interne.
## Fabric.js Annotation Editor (`client/static/js/annotation-editor.js`)
## Fabric.js Annotation Editor (`src/frontend/flask_app/static/js/annotation-editor.js`)
Editor annotazioni su disegni tecnici (Fabric.js 5.3.1, ~1200 righe). Pattern critici:
@@ -152,16 +219,16 @@ Le ricette usano un versioning condizionale. L'endpoint `PUT /api/recipes/{id}`
La stessa logica si applica nel task router: aggiungere un task a una ricetta con measurements crea una nuova versione.
La versione corrente ha `is_current=True`, le precedenti `False`. Audit trail in `recipe_version_audit` (CREATE, UPDATE, ACTIVATE, RETIRE). Logica in `server/services/recipe_service.py`.
La versione corrente ha `is_current=True`, le precedenti `False`. Audit trail in `recipe_version_audit` (CREATE, UPDATE, ACTIVATE, RETIRE). Logica in `src/backend/services/recipe_service.py`.
### Calcolo Pass/Fail
Ogni subtask ha 4 limiti di tolleranza: UTL (upper tolerance), UWL (upper warning), LWL (lower warning), LTL (lower tolerance) più un valore nominale. Il calcolo in `server/services/measurement_service.py`:
Ogni subtask ha 4 limiti di tolleranza: UTL (upper tolerance), UWL (upper warning), LWL (lower warning), LTL (lower tolerance) più un valore nominale. Il calcolo in `src/backend/services/measurement_service.py`:
- Fuori UTL/LTL → **fail**
- Fuori UWL/LWL ma dentro UTL/LTL → **warning**
- Dentro UWL/LWL → **pass**
### SPC (Statistical Process Control)
Calcoli in `server/services/spc_service.py` usando solo `math` e `statistics` stdlib (no numpy/scipy): summary (conteggi pass/warning/fail), capability (Cp, Cpk, Pp, Ppk), control chart (UCL/LCL = mean ± 3σ), histogram (20 bin + curva normale).
Calcoli in `src/backend/services/spc_service.py` usando solo `math` e `statistics` stdlib (no numpy/scipy): summary (conteggi pass/warning/fail), capability (Cp, Cpk, Pp, Ppk), control chart (UCL/LCL = mean ± 3σ), histogram (20 bin + curva normale).
### Autenticazione
1. Login con username/password → server ritorna `api_key` (64 char random)
@@ -187,7 +254,7 @@ Upload in `uploads/{recipe_id}/{version_id}/`. Tipi ammessi: JPEG, PNG, GIF, Web
## Test Infrastructure
### Server (`server/tests/conftest.py`)
### Server (`src/backend/tests/conftest.py`)
- SQLite in-memory `sqlite+aiosqlite://` con `StaticPool` (singola connessione condivisa tra fixture, app e test)
- WeasyPrint mockato prima di qualsiasi import server: `sys.modules["weasyprint"] = MagicMock()`
- Rate limit buckets resettati tra test (walk middleware stack → clear dicts)
@@ -196,7 +263,7 @@ Upload in `uploads/{recipe_id}/{version_id}/`. Tipi ammessi: JPEG, PNG, GIF, Web
- Helper: `auth_headers(user)``{"X-API-Key": user.api_key}`
- Client httpx: `AsyncClient` con `ASGITransport(app=app)`, override di `get_db` dependency
### Client (`client/tests/conftest.py`)
### Client (`src/frontend/flask_app/tests/conftest.py`)
- `api_client` patchato in 4 blueprint (auth, maker, measure, statistics). **admin NON è patchato** — i test admin devono gestire il mock manualmente
- `logged_in_client` fixture pre-popola session con `api_key`, `user_id`, `language`, `theme` + user dict
- CSRF disabilitato nei test: `WTF_CSRF_ENABLED=False`
@@ -232,7 +299,7 @@ Variabili d'ambiente in `.env` (copiare da `.env.example`):
- DB: `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `DB_ROOT_PASSWORD` (Docker)
- Server: `SERVER_HOST`, `SERVER_PORT`, `SERVER_SECRET_KEY`, `SERVER_CORS_ORIGINS`
- Client: `CLIENT_HOST`, `CLIENT_PORT`, `CLIENT_SECRET_KEY`, `API_SERVER_URL`
- Upload: `UPLOAD_DIR` (default `"uploads"`, relativo a `server/`), `MAX_UPLOAD_SIZE_MB`
- Upload: `UPLOAD_DIR` (default `"uploads"`, relativo a `src/backend/`), `MAX_UPLOAD_SIZE_MB`
- Docker: `NGINX_PORT`, `NGINX_SSL_PORT`
- Setup: `SETUP_PASSWORD` (vuota = endpoint disabilitato)
- SSL: `SSL_CERTFILE`, `SSL_KEYFILE`