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:
@@ -0,0 +1,178 @@
|
||||
"""Metrologist blueprint - SPC statistics dashboard and API proxies."""
|
||||
import requests as http_requests
|
||||
|
||||
from flask import Blueprint, jsonify, render_template, request, session
|
||||
from flask_babel import gettext as _
|
||||
|
||||
from blueprints.auth import login_required, role_required
|
||||
from config import Config
|
||||
from services.api_client import api_client
|
||||
|
||||
statistics_bp = Blueprint("statistics", __name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PAGINE
|
||||
# ============================================================================
|
||||
|
||||
@statistics_bp.route("/dashboard")
|
||||
@login_required
|
||||
@role_required("Metrologist")
|
||||
def dashboard():
|
||||
"""SPC dashboard overview — loads recipes for filter dropdown."""
|
||||
resp = api_client.get("/api/recipes", params={"per_page": 100})
|
||||
if isinstance(resp, dict) and resp.get("error"):
|
||||
recipes = []
|
||||
else:
|
||||
recipes = resp.get("items", []) if isinstance(resp, dict) else []
|
||||
|
||||
return render_template("statistics/dashboard.html", recipes=recipes)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PROXY API AJAX → FastAPI
|
||||
# ============================================================================
|
||||
|
||||
def _proxy_statistics(endpoint: str):
|
||||
"""Forward query params to a FastAPI statistics endpoint."""
|
||||
params = {}
|
||||
for key in [
|
||||
"recipe_id", "version_id", "subtask_id",
|
||||
"date_from", "date_to", "operator_id",
|
||||
"lot_number", "serial_number", "n_bins",
|
||||
]:
|
||||
val = request.args.get(key)
|
||||
if val is not None and val != "":
|
||||
params[key] = val
|
||||
|
||||
resp = api_client.get(f"/api/statistics/{endpoint}", params=params)
|
||||
|
||||
if isinstance(resp, dict) and resp.get("error"):
|
||||
return jsonify({"error": True, "detail": resp.get("detail", "")}), resp.get("status_code", 500)
|
||||
|
||||
return jsonify(resp)
|
||||
|
||||
|
||||
@statistics_bp.route("/api/summary")
|
||||
@login_required
|
||||
@role_required("Metrologist")
|
||||
def api_summary():
|
||||
"""Proxy: summary pass/fail/warning."""
|
||||
return _proxy_statistics("summary")
|
||||
|
||||
|
||||
@statistics_bp.route("/api/capability")
|
||||
@login_required
|
||||
@role_required("Metrologist")
|
||||
def api_capability():
|
||||
"""Proxy: capability indices."""
|
||||
return _proxy_statistics("capability")
|
||||
|
||||
|
||||
@statistics_bp.route("/api/control-chart")
|
||||
@login_required
|
||||
@role_required("Metrologist")
|
||||
def api_control_chart():
|
||||
"""Proxy: control chart data."""
|
||||
return _proxy_statistics("control-chart")
|
||||
|
||||
|
||||
@statistics_bp.route("/api/histogram")
|
||||
@login_required
|
||||
@role_required("Metrologist")
|
||||
def api_histogram():
|
||||
"""Proxy: histogram data."""
|
||||
return _proxy_statistics("histogram")
|
||||
|
||||
|
||||
@statistics_bp.route("/api/subtasks")
|
||||
@login_required
|
||||
@role_required("Metrologist")
|
||||
def api_subtasks():
|
||||
"""Proxy: get subtasks for recipe filter."""
|
||||
params = {}
|
||||
recipe_id = request.args.get("recipe_id")
|
||||
if recipe_id:
|
||||
params["recipe_id"] = recipe_id
|
||||
version_id = request.args.get("version_id")
|
||||
if version_id:
|
||||
params["version_id"] = version_id
|
||||
|
||||
resp = api_client.get("/api/statistics/subtasks", params=params)
|
||||
|
||||
if isinstance(resp, dict) and resp.get("error"):
|
||||
return jsonify({"error": True, "detail": resp.get("detail", "")}), resp.get("status_code", 500)
|
||||
|
||||
# Response is a list
|
||||
if isinstance(resp, list):
|
||||
return jsonify(resp)
|
||||
|
||||
return jsonify(resp)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PROXY REPORT PDF → FastAPI
|
||||
# ============================================================================
|
||||
|
||||
def _proxy_report(endpoint: str):
|
||||
"""Forward query params to a FastAPI reports endpoint and return PDF."""
|
||||
params = {}
|
||||
for key in [
|
||||
"recipe_id", "version_id", "subtask_id",
|
||||
"date_from", "date_to", "operator_id",
|
||||
"lot_number", "serial_number",
|
||||
]:
|
||||
val = request.args.get(key)
|
||||
if val is not None and val != "":
|
||||
params[key] = val
|
||||
|
||||
base_url = Config.API_SERVER_URL.rstrip("/")
|
||||
headers = {}
|
||||
api_key = session.get("api_key")
|
||||
if api_key:
|
||||
headers["X-API-Key"] = api_key
|
||||
|
||||
try:
|
||||
resp = http_requests.get(
|
||||
f"{base_url}/api/reports/{endpoint}",
|
||||
headers=headers,
|
||||
params=params,
|
||||
timeout=120,
|
||||
)
|
||||
except (http_requests.ConnectionError, http_requests.Timeout) as e:
|
||||
return jsonify({"error": True, "detail": str(e)}), 502
|
||||
|
||||
if not resp.ok:
|
||||
try:
|
||||
err = resp.json()
|
||||
detail = err.get("detail", f"HTTP {resp.status_code}")
|
||||
except Exception:
|
||||
detail = f"HTTP {resp.status_code}"
|
||||
return jsonify({"error": True, "detail": detail}), resp.status_code
|
||||
|
||||
from flask import Response as FlaskResponse
|
||||
return FlaskResponse(
|
||||
resp.content,
|
||||
mimetype="application/pdf",
|
||||
headers={
|
||||
"Content-Disposition": resp.headers.get(
|
||||
"Content-Disposition", f'attachment; filename="report.pdf"'
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@statistics_bp.route("/api/report-spc")
|
||||
@login_required
|
||||
@role_required("Metrologist")
|
||||
def api_report_spc():
|
||||
"""Proxy: download SPC PDF report."""
|
||||
return _proxy_report("spc")
|
||||
|
||||
|
||||
@statistics_bp.route("/api/report-measurements")
|
||||
@login_required
|
||||
@role_required("Metrologist")
|
||||
def api_report_measurements():
|
||||
"""Proxy: download measurements PDF report."""
|
||||
return _proxy_report("measurements")
|
||||
Reference in New Issue
Block a user