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
+218
View File
@@ -0,0 +1,218 @@
/**
* Numpad Component - Alpine.js component for touch-friendly numeric input
* Used for measurement data entry in task_execute.html
*/
function numpad() {
return {
// State
value: '', // String representation of the current value
negative: false, // Whether the value is negative
hasDecimal: false, // Whether a decimal point has been entered
unit: 'mm', // Unit of measurement (can be set externally)
maxIntDigits: 6, // Maximum integer digits
maxDecDigits: 6, // Maximum decimal digits
// HID burst detection (USB caliper vs manual typing)
_lastKeyTime: 0, // Timestamp of last keystroke
_burstCount: 0, // Consecutive fast keystrokes
/**
* Get the display value with sign
*/
get displayValue() {
if (!this.value) return '';
return (this.negative ? '-' : '') + this.value;
},
/**
* Get the numeric value as a number
*/
get numericValue() {
if (!this.value) return null;
const v = parseFloat(this.value);
return this.negative ? -v : v;
},
/**
* Check if a valid value has been entered
*/
get hasValue() {
return this.value.length > 0 && this.value !== '.';
},
/**
* Add a digit to the current value
* @param {string} d - The digit to add (0-9)
*/
addDigit(d) {
// Validate: don't exceed max digits
const parts = this.value.split('.');
if (this.hasDecimal) {
// Check decimal part length
if (parts[1] && parts[1].length >= this.maxDecDigits) return;
} else {
// Check integer part length
if (parts[0] && parts[0].length >= this.maxIntDigits) return;
}
// Prevent leading zeros (except "0.")
if (this.value === '0' && d !== '.') {
this.value = d;
return;
}
this.value += d;
},
/**
* Add a decimal point to the current value
*/
addDecimal() {
// Don't add decimal if one already exists
if (this.hasDecimal) return;
// If value is empty, start with "0."
if (!this.value) this.value = '0';
this.value += '.';
this.hasDecimal = true;
},
/**
* Toggle the sign of the current value
*/
toggleSign() {
if (!this.value) return;
this.negative = !this.negative;
},
/**
* Remove the last character from the current value
*/
backspace() {
if (!this.value) return;
const removed = this.value.charAt(this.value.length - 1);
// If removing decimal point, update flag
if (removed === '.') this.hasDecimal = false;
this.value = this.value.slice(0, -1);
},
/**
* Clear all entered data
*/
clearAll() {
this.value = '';
this.negative = false;
this.hasDecimal = false;
},
/**
* Confirm the current value and dispatch event
*/
confirm() {
if (!this.hasValue) return;
const val = this.numericValue;
// Determine input method: 3+ fast keystrokes = USB caliper burst
const inputMethod = this._burstCount >= 3 ? 'usb_caliper' : 'manual';
// Dispatch custom event for parent component to handle
this.$dispatch('numpad-confirm', { value: val, inputMethod: inputMethod });
// Reset after confirmation
this.clearAll();
this._burstCount = 0;
this._lastKeyTime = 0;
},
/**
* Set a value programmatically (e.g., from USB caliper)
* @param {number} val - The numeric value to set
*/
setValue(val) {
this.clearAll();
// Handle negative values
if (val < 0) {
this.negative = true;
val = Math.abs(val);
}
this.value = val.toString();
// Update decimal flag if value contains decimal point
if (this.value.includes('.')) this.hasDecimal = true;
},
/**
* Set the unit of measurement
* @param {string} newUnit - The unit to set (e.g., 'mm', 'cm', 'in')
*/
setUnit(newUnit) {
this.unit = newUnit;
},
/**
* Handle keyboard input for physical keyboard support
* @param {KeyboardEvent} e - The keyboard event
*/
handleKeydown(e) {
// Number keys
if (e.key >= '0' && e.key <= '9') {
e.preventDefault();
this._trackBurst();
this.addDigit(e.key);
}
// Decimal point (both . and ,)
else if (e.key === '.' || e.key === ',') {
e.preventDefault();
this._trackBurst();
this.addDecimal();
}
// Backspace
else if (e.key === 'Backspace') {
e.preventDefault();
this.backspace();
}
// Escape - clear all
else if (e.key === 'Escape') {
e.preventDefault();
this.clearAll();
}
// Enter - confirm
else if (e.key === 'Enter') {
e.preventDefault();
this.confirm();
}
// Minus sign - toggle sign
else if (e.key === '-') {
e.preventDefault();
this.toggleSign();
}
},
/**
* Track keystroke timing for HID burst detection.
* USB calipers send digits in rapid succession (<80ms apart).
* Human typing is much slower (>100ms between keys).
*/
_trackBurst() {
const now = performance.now();
const gap = now - this._lastKeyTime;
if (this._lastKeyTime > 0 && gap < 80) {
this._burstCount++;
} else {
this._burstCount = 1;
}
this._lastKeyTime = now;
}
};
}