feat: FASE 0 - Setup progetto TieMeasureFlow

Struttura monorepo completa con server FastAPI e client Flask:
- Server: FastAPI + SQLAlchemy 2.0 async + Alembic migrations
- Client: Flask + blueprints (auth, measure, maker, statistics)
- Database: docker-compose MySQL 8.0 + Alembic async config
- Config: pydantic-settings, TailwindCSS, Flask-Babel i18n
- Piano implementazione completo (18 sezioni, 1600 righe)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Adriano
2026-02-07 00:16:54 +01:00
commit dbdbb77daf
47 changed files with 2489 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
"""TieMeasureFlow Client - Flask Entry Point."""
import os
from flask import Flask, redirect, url_for, session, request
from flask_babel import Babel
from config import Config
def get_locale():
"""Get user's preferred language from session or Accept-Language header."""
# 1. User preference in session
if "language" in session:
return session["language"]
# 2. Browser Accept-Language
return request.accept_languages.best_match(
Config.LANGUAGES.keys(), default="it"
)
def create_app() -> Flask:
"""Application factory."""
app = Flask(__name__)
app.config.from_object(Config)
# Initialize Flask-Babel
Babel(app, locale_selector=get_locale)
# Register blueprints
from blueprints.auth import auth_bp
from blueprints.measure import measure_bp
from blueprints.maker import maker_bp
from blueprints.statistics import statistics_bp
app.register_blueprint(auth_bp)
app.register_blueprint(measure_bp, url_prefix="/measure")
app.register_blueprint(maker_bp, url_prefix="/maker")
app.register_blueprint(statistics_bp, url_prefix="/statistics")
@app.route("/")
def index():
"""Root redirect to login or dashboard based on session."""
if "user" in session:
return redirect(url_for("measure.select_recipe"))
return redirect(url_for("auth.login"))
@app.context_processor
def inject_globals():
"""Inject global variables into all templates."""
return {
"current_user": session.get("user"),
"current_theme": session.get("theme", "light"),
"current_language": get_locale(),
"languages": Config.LANGUAGES,
}
return app
if __name__ == "__main__":
app = create_app()
app.run(
host=os.getenv("CLIENT_HOST", "0.0.0.0"),
port=int(os.getenv("CLIENT_PORT", "5000")),
debug=True,
)
View File
+31
View File
@@ -0,0 +1,31 @@
"""Authentication blueprint - login, logout, profile."""
from flask import Blueprint, render_template, request, redirect, url_for, session, flash
auth_bp = Blueprint("auth", __name__)
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
"""Login page."""
if request.method == "POST":
# TODO: Implement API call to server /api/auth/login
pass
return render_template("auth/login.html")
@auth_bp.route("/logout")
def logout():
"""Logout - clear session."""
session.clear()
return redirect(url_for("auth.login"))
@auth_bp.route("/profile", methods=["GET", "POST"])
def profile():
"""User profile - change display name, language, theme."""
if "user" not in session:
return redirect(url_for("auth.login"))
if request.method == "POST":
# TODO: Implement API call to server /api/auth/me PUT
pass
return render_template("auth/profile.html")
+45
View File
@@ -0,0 +1,45 @@
"""Maker blueprint - recipe creation and editing."""
from flask import Blueprint, render_template, session, redirect, url_for
maker_bp = Blueprint("maker", __name__)
@maker_bp.route("/recipes")
def recipe_list():
"""List all recipes with filters."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("maker/recipe_list.html")
@maker_bp.route("/recipes/new")
@maker_bp.route("/recipes/<int:recipe_id>/edit")
def recipe_editor(recipe_id: int | None = None):
"""Recipe editor - create or edit."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("maker/recipe_editor.html", recipe_id=recipe_id)
@maker_bp.route("/recipes/<int:recipe_id>/tasks")
def task_editor(recipe_id: int):
"""Task/subtask editor with tolerances."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("maker/task_editor.html", recipe_id=recipe_id)
@maker_bp.route("/recipes/<int:recipe_id>/preview")
def recipe_preview(recipe_id: int):
"""Preview recipe as MeasurementTec would see it."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("maker/recipe_preview.html", recipe_id=recipe_id)
@maker_bp.route("/recipes/<int:recipe_id>/versions")
def version_history(recipe_id: int):
"""Version history with diff."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("maker/version_history.html", recipe_id=recipe_id)
+36
View File
@@ -0,0 +1,36 @@
"""MeasurementTec blueprint - recipe selection and measurement execution."""
from flask import Blueprint, render_template, session, redirect, url_for
measure_bp = Blueprint("measure", __name__)
@measure_bp.route("/select")
def select_recipe():
"""Recipe selection page."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("measure/select_recipe.html")
@measure_bp.route("/tasks/<int:recipe_id>")
def task_list(recipe_id: int):
"""Task list for selected recipe."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("measure/task_list.html", recipe_id=recipe_id)
@measure_bp.route("/execute/<int:task_id>")
def task_execute(task_id: int):
"""Execute measurements for a task."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("measure/task_execute.html", task_id=task_id)
@measure_bp.route("/complete/<int:recipe_id>")
def task_complete(recipe_id: int):
"""Task completion summary."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("measure/task_complete.html", recipe_id=recipe_id)
+44
View File
@@ -0,0 +1,44 @@
"""Metrologist blueprint - SPC statistics and dashboards."""
from flask import Blueprint, render_template, session, redirect, url_for
statistics_bp = Blueprint("statistics", __name__)
@statistics_bp.route("/dashboard")
def dashboard():
"""SPC dashboard overview."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("statistics/dashboard.html")
@statistics_bp.route("/control-chart")
def control_chart():
"""X-bar / R control chart."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("statistics/control_chart.html")
@statistics_bp.route("/histogram")
def histogram():
"""Histogram with normal curve."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("statistics/histogram.html")
@statistics_bp.route("/capability")
def capability():
"""Cp/Cpk/Pp/Ppk capability gauge."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("statistics/capability.html")
@statistics_bp.route("/trend")
def trend():
"""Temporal trends and period comparison."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("statistics/trend.html")
+24
View File
@@ -0,0 +1,24 @@
"""TieMeasureFlow Client Configuration."""
import os
from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env'))
class Config:
"""Flask client configuration."""
# Flask
SECRET_KEY = os.getenv("CLIENT_SECRET_KEY", "change-this-to-another-random-secret-key")
# API Server connection
API_SERVER_URL = os.getenv("API_SERVER_URL", "http://localhost:8000")
# Session
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
# Babel i18n
BABEL_DEFAULT_LOCALE = "it"
BABEL_DEFAULT_TIMEZONE = "Europe/Rome"
LANGUAGES = {"it": "Italiano", "en": "English"}
+10
View File
@@ -0,0 +1,10 @@
# Flask
flask>=3.0.0
flask-babel>=4.0.0
# HTTP Client (to call FastAPI server)
requests>=2.31.0
urllib3>=2.0.0
# Utilities
python-dotenv>=1.0.0
View File
+80
View File
@@ -0,0 +1,80 @@
"""API Client - wrapper for HTTP requests to FastAPI server."""
from typing import Any
import requests
from flask import session
from config import Config
class APIClient:
"""HTTP client for TieMeasureFlow API server."""
def __init__(self):
self.base_url = Config.API_SERVER_URL.rstrip("/")
self.timeout = 30
@property
def _headers(self) -> dict[str, str]:
"""Build request headers with API key from session."""
headers = {"Content-Type": "application/json"}
api_key = session.get("api_key")
if api_key:
headers["X-API-Key"] = api_key
return headers
def get(self, endpoint: str, params: dict | None = None) -> dict[str, Any]:
"""GET request to API server."""
response = requests.get(
f"{self.base_url}{endpoint}",
headers=self._headers,
params=params,
timeout=self.timeout,
)
response.raise_for_status()
return response.json()
def post(self, endpoint: str, data: dict | None = None, files: dict | None = None) -> dict[str, Any]:
"""POST request to API server."""
if files:
headers = {"X-API-Key": session.get("api_key", "")}
response = requests.post(
f"{self.base_url}{endpoint}",
headers=headers,
data=data,
files=files,
timeout=self.timeout,
)
else:
response = requests.post(
f"{self.base_url}{endpoint}",
headers=self._headers,
json=data,
timeout=self.timeout,
)
response.raise_for_status()
return response.json()
def put(self, endpoint: str, data: dict | None = None) -> dict[str, Any]:
"""PUT request to API server."""
response = requests.put(
f"{self.base_url}{endpoint}",
headers=self._headers,
json=data,
timeout=self.timeout,
)
response.raise_for_status()
return response.json()
def delete(self, endpoint: str) -> dict[str, Any]:
"""DELETE request to API server."""
response = requests.delete(
f"{self.base_url}{endpoint}",
headers=self._headers,
timeout=self.timeout,
)
response.raise_for_status()
return response.json()
api_client = APIClient()
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
View File
View File
View File
+34
View File
@@ -0,0 +1,34 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./templates/**/*.html",
"./static/js/**/*.js",
],
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#2563EB',
dark: '#1E40AF',
light: '#3B82F6',
},
steel: {
DEFAULT: '#64748B',
light: '#94A3B8',
dark: '#475569',
},
measure: {
pass: '#059669',
warning: '#D97706',
fail: '#DC2626',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
},
},
plugins: [],
}
View File
View File
View File
View File
+3
View File
@@ -0,0 +1,3 @@
[python: blueprints/**.py]
[jinja2: templates/**.html]
encoding = utf-8