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:
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user