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
@@ -0,0 +1,296 @@
/**
* SPC Charts — Plotly.js helper functions for Metrologist dashboard.
*
* Requires Plotly.js loaded via CDN before this script.
* Requires window.SPC_I18N object set in the template for translations.
*
* Brand colors:
* Primary: #2563EB
* Pass: #10b981
* Warning: #f59e0b
* Fail: #ef4444
*/
// i18n helper — falls back to Italian defaults
const _t = (key) => (window.SPC_I18N && window.SPC_I18N[key]) || key;
const SPC_COLORS = {
primary: '#2563EB',
primaryLight: '#3B82F6',
pass: '#10b981',
warning: '#f59e0b',
fail: '#ef4444',
steel: '#64748B',
steelLight: '#94A3B8',
nominal: '#8b5cf6',
ucl_lcl: '#ef4444',
uwl_lwl: '#f59e0b',
utl_ltl: '#dc2626',
bg: '#ffffff',
gridColor: '#e2e8f0',
textColor: '#334155',
};
const PLOTLY_LAYOUT_DEFAULTS = {
font: { family: 'Inter, system-ui, sans-serif', color: SPC_COLORS.textColor },
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
margin: { t: 20, r: 30, b: 50, l: 60 },
xaxis: { gridcolor: SPC_COLORS.gridColor, zeroline: false },
yaxis: { gridcolor: SPC_COLORS.gridColor, zeroline: false },
};
const PLOTLY_CONFIG = {
responsive: true,
displayModeBar: 'hover',
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
displaylogo: false,
};
/**
* Render a control chart (X-bar) with tolerance and control limits.
*
* @param {string} containerId - DOM element ID for the chart.
* @param {Object} data - ControlChartData from API.
*/
function renderControlChart(containerId, data) {
if (!data || !data.values || data.values.length === 0) {
document.getElementById(containerId).innerHTML =
'<p class="text-center text-steel py-12">' + _t('noData') + '</p>';
return;
}
const n = data.values.length;
const xLabels = data.timestamps.map((t, i) => i + 1);
// Separate in-control and out-of-control points
const oocSet = new Set(data.out_of_control || []);
const inControlX = [], inControlY = [];
const oocX = [], oocY = [];
for (let i = 0; i < n; i++) {
if (oocSet.has(i)) {
oocX.push(xLabels[i]);
oocY.push(data.values[i]);
} else {
inControlX.push(xLabels[i]);
inControlY.push(data.values[i]);
}
}
const traces = [
// In-control points
{
x: inControlX, y: inControlY,
mode: 'markers',
type: 'scatter',
name: _t('inControl'),
marker: { color: SPC_COLORS.primary, size: 6 },
},
// Out-of-control points
{
x: oocX, y: oocY,
mode: 'markers',
type: 'scatter',
name: _t('outOfControl'),
marker: { color: SPC_COLORS.fail, size: 9, symbol: 'diamond' },
},
// Line connecting all points
{
x: xLabels, y: data.values,
mode: 'lines',
type: 'scatter',
name: '',
line: { color: SPC_COLORS.steelLight, width: 1 },
showlegend: false,
},
];
// Helper to add horizontal line
const shapes = [];
function addLine(yVal, color, dash) {
if (yVal == null) return;
shapes.push({
type: 'line', xref: 'paper', x0: 0, x1: 1, y0: yVal, y1: yVal,
line: { color, width: 1.5, dash },
});
}
// Mean line
addLine(data.mean, SPC_COLORS.primary, 'solid');
// UCL / LCL
addLine(data.ucl, SPC_COLORS.ucl_lcl, 'dash');
addLine(data.lcl, SPC_COLORS.ucl_lcl, 'dash');
// UTL / LTL
addLine(data.utl, SPC_COLORS.utl_ltl, 'dot');
addLine(data.ltl, SPC_COLORS.utl_ltl, 'dot');
// UWL / LWL
addLine(data.uwl, SPC_COLORS.uwl_lwl, 'dashdot');
addLine(data.lwl, SPC_COLORS.uwl_lwl, 'dashdot');
// Nominal
addLine(data.nominal, SPC_COLORS.nominal, 'longdash');
// Build annotations for limit labels
const annotations = [];
function addLabel(yVal, text, color) {
if (yVal == null) return;
annotations.push({
x: 1, xref: 'paper', xanchor: 'left',
y: yVal, yanchor: 'middle',
text: text,
showarrow: false,
font: { size: 10, color },
});
}
addLabel(data.mean, 'X\u0304', SPC_COLORS.primary);
addLabel(data.ucl, 'UCL', SPC_COLORS.ucl_lcl);
addLabel(data.lcl, 'LCL', SPC_COLORS.ucl_lcl);
addLabel(data.utl, 'UTL', SPC_COLORS.utl_ltl);
addLabel(data.ltl, 'LTL', SPC_COLORS.utl_ltl);
addLabel(data.uwl, 'UWL', SPC_COLORS.uwl_lwl);
addLabel(data.lwl, 'LWL', SPC_COLORS.uwl_lwl);
addLabel(data.nominal, 'NOM', SPC_COLORS.nominal);
const layout = {
...PLOTLY_LAYOUT_DEFAULTS,
xaxis: { ...PLOTLY_LAYOUT_DEFAULTS.xaxis, title: _t('measureNum') },
yaxis: { ...PLOTLY_LAYOUT_DEFAULTS.yaxis, title: _t('value') },
shapes,
annotations,
showlegend: true,
legend: { orientation: 'h', y: -0.15 },
margin: { ...PLOTLY_LAYOUT_DEFAULTS.margin, r: 50 },
};
Plotly.newPlot(containerId, traces, layout, PLOTLY_CONFIG);
}
/**
* Render histogram with normal curve overlay.
*
* @param {string} containerId - DOM element ID.
* @param {Object} data - HistogramData from API.
* @param {Object} [tol] - Optional tolerance limits {utl, ltl, nominal}.
*/
function renderHistogram(containerId, data, tol) {
if (!data || !data.bins || data.bins.length === 0 || data.n === 0) {
document.getElementById(containerId).innerHTML =
'<p class="text-center text-steel py-12">' + _t('noData') + '</p>';
return;
}
// Bin centers for bar chart
const binCenters = [];
const binWidth = data.bins[1] - data.bins[0];
for (let i = 0; i < data.counts.length; i++) {
binCenters.push((data.bins[i] + data.bins[i + 1]) / 2);
}
const traces = [
// Histogram bars
{
x: binCenters, y: data.counts,
type: 'bar',
name: _t('frequency'),
marker: { color: SPC_COLORS.primaryLight, opacity: 0.7 },
width: binWidth * 0.9,
},
];
// Normal curve overlay
if (data.normal_x && data.normal_x.length > 0) {
traces.push({
x: data.normal_x, y: data.normal_y,
type: 'scatter',
mode: 'lines',
name: _t('normalCurve'),
line: { color: SPC_COLORS.fail, width: 2 },
});
}
const shapes = [];
function addVertLine(xVal, color, dash) {
if (xVal == null) return;
shapes.push({
type: 'line', yref: 'paper', y0: 0, y1: 1, x0: xVal, x1: xVal,
line: { color, width: 1.5, dash },
});
}
if (tol) {
addVertLine(tol.utl, SPC_COLORS.utl_ltl, 'dot');
addVertLine(tol.ltl, SPC_COLORS.utl_ltl, 'dot');
addVertLine(tol.nominal, SPC_COLORS.nominal, 'longdash');
}
// Mean line
addVertLine(data.mean, SPC_COLORS.primary, 'dash');
const layout = {
...PLOTLY_LAYOUT_DEFAULTS,
xaxis: { ...PLOTLY_LAYOUT_DEFAULTS.xaxis, title: _t('value') },
yaxis: { ...PLOTLY_LAYOUT_DEFAULTS.yaxis, title: _t('frequency') },
shapes,
bargap: 0.05,
showlegend: true,
legend: { orientation: 'h', y: -0.15 },
};
Plotly.newPlot(containerId, traces, layout, PLOTLY_CONFIG);
}
/**
* Render a capability gauge (horizontal bar indicator for Cpk).
*
* @param {string} containerId - DOM element ID.
* @param {Object} data - CapabilityData from API.
*/
function renderCapabilityGauge(containerId, data) {
if (!data || data.cpk == null) {
document.getElementById(containerId).innerHTML =
'<p class="text-center text-steel py-8">' + _t('indicesNotAvailable') + '</p>';
return;
}
const cpk = data.cpk;
// Color zones: <1.0 = fail, 1.0-1.33 = warning, >=1.33 = pass
let barColor = SPC_COLORS.fail;
if (cpk >= 1.33) barColor = SPC_COLORS.pass;
else if (cpk >= 1.0) barColor = SPC_COLORS.warning;
const trace = {
type: 'indicator',
mode: 'gauge+number',
value: cpk,
title: { text: 'Cpk', font: { size: 14 } },
number: { font: { family: 'JetBrains Mono, monospace', size: 28 } },
gauge: {
axis: { range: [0, 2.5], tickwidth: 1 },
bar: { color: barColor, thickness: 0.6 },
bgcolor: '#f1f5f9',
borderwidth: 0,
steps: [
{ range: [0, 1.0], color: '#fecaca' },
{ range: [1.0, 1.33], color: '#fef3c7' },
{ range: [1.33, 2.5], color: '#d1fae5' },
],
threshold: {
line: { color: SPC_COLORS.primary, width: 3 },
thickness: 0.75,
value: cpk,
},
},
};
const layout = {
...PLOTLY_LAYOUT_DEFAULTS,
height: 200,
margin: { t: 30, r: 20, b: 10, l: 20 },
};
Plotly.newPlot(containerId, [trace], layout, { ...PLOTLY_CONFIG, displayModeBar: false });
}