Commit Graph

64 Commits

Author SHA1 Message Date
Adriano 8f9731839a backend: camera e modo di analisi nel task, /vision/check senza salvataggio (Refs #97)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 18:20:14 +02:00
Adriano ad6ce5d11d Backend: ricetta DXF compare da camera e calibrazione letta dall'acquisitore (#83)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 22:44:15 +02:00
Adriano f8f5f248ff setup: init-db e reset-db stampano head in un thread (TieMeasureFlow#80)
_stamp_head esegue env.py di alembic, che fa asyncio.run() e dentro il loop
di FastAPI dava RuntimeError (500). Ora gira con asyncio.to_thread. Nuovo
test_setup_stamp.py senza monkeypatch di _stamp_head, su file SQLite.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 14:25:24 +02:00
Adriano 1453b31cee by-code: auth da utente loggato qualunque, come /recipes (#72)
Lo chiama il proxy Flask con la chiave dell'operatore MeasurementTec, quindi
get_current_user al posto di require_admin_user. Test: MeasurementTec 200,
senza chiave 401.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 09:53:33 +02:00
Adriano b9f43a0ede Stazioni: camera_url e camera_source, GET /api/stations/by-code/{code} (#72)
Migrazione 020 con le due colonne su stations, campi in modello, schemi e
servizio (validatore http(s) come vision_worker_url), nuova route by-code
dichiarata prima di /{station_id}. Test di migrazione su e giu e di API.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 09:50:14 +02:00
Adriano c911449b7c Statistica: endpoint esiti, note, campi e allegati dei task di una sessione (#58)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NABedeMX43pyXZbDNUgA5B
2026-10-11 18:07:06 +00:00
Adriano 21f8bae5ac Task results: upload allegati per MeasurementTec, percorsi in task_results/, test e migrazione 018 (#55 #53 #54)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 12:50:42 +02:00
Adriano df1727be73 Task: in_loop, at_close, fields, tipo check, task_results, close_task_ids, rate limit login-pin (#53 #54 #55 #59)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 12:42:13 +02:00
Adriano 743f246cbf Merge lavoro/daitarn-trafilo-pin 2026-10-07 12:38:56 +02:00
Adriano f7d6d008c7 Merge lavoro/daitarn-trafilo-duplica 2026-10-07 12:33:02 +02:00
Adriano 837f3d1069 Merge lavoro/daitarn-trafilo-statsessione 2026-10-07 12:33:02 +02:00
Adriano f5a2affa0f Merge lavoro/daitarn-trafilo-monitor 2026-10-07 12:33:02 +02:00
Adriano 3b3ecceb2a Merge lavoro/daitarn-trafilo-settings 2026-10-07 12:33:02 +02:00
Adriano e44ac3129f Statistiche sessioni: conteggio misure solo sui run della ricetta (#58)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 12:31:53 +02:00
Adriano 4e52095423 Statistiche per sessione di produzione: filtro production_run_id, elenco sessioni, select Sessione (#58)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 12:28:35 +02:00
Adriano 23bfc93f1c Impostazioni: durata loop, tolleranze, anagrafiche; tab Utenti con reset password (#47 #48 #49 #50 #51)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 12:28:31 +02:00
Adriano 07e23b957d PIN a 4 cifre: migrazione 017, login-pin, pin-users, PUT pin, capoturno via PIN (#59)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 12:27:31 +02:00
Adriano 3c1d5f1323 Duplica ricetta: servizio, endpoint e test backend (#52)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 12:25:25 +02:00
Adriano db5d686938 Monitor stazioni: GET /api/production-runs/open (#57)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 12:25:16 +02:00
Adriano ee0f65ed39 Endpoint ricetta VisionSuite v2, execute con overlay e immagine salvata (#46)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 12:12:50 +02:00
Adriano 7ac291b8ac Backend: date dei filtri dei report e utenti setup in ora italiana/UTC esplicito (#42)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 11:59:07 +02:00
Adriano 486e1288bf Backend: UTC nel DB, +00:00 nelle API, ora italiana solo in presentazione (#42)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 11:41:22 +02:00
Adriano 35f46926a9 API key come hash SHA-256, una per sessione, TTL rinnovabile (#41)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 11:34:30 +02:00
Adriano 1d93eded27 Ripristino delle quote già misurate nel ciclo in corso dopo un reload (#40)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 11:31:57 +02:00
Adriano 70e2072b79 File di riferimento rimossi con il task, immagini animate rifiutate, filtro production_run_id (#43)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 11:27:23 +02:00
Adriano 96dd8d6ea3 fix(vision): reference image sopravvive al copy-on-write (#38.6)
_copy_tasks_to_version non copiava le righe VisionReferenceImage del
task sorgente: il task copiato perdeva le immagini di riferimento del
grafo di visione. Ora copia le righe sul task nuovo, stesso path e
stessi metadati (stesso file su disco, non duplicato).
Di conseguenza il file va cancellato solo se nessun'altra riga
VisionReferenceImage referenzia lo stesso path: nuova
vision_service.file_still_referenced(), usata dalla route DELETE
prima di remove_reference_file.
Test: copy-on-write elenca l'immagine sulla nuova versione; DELETE di
una delle due righe lascia il file; DELETE dell'ultima lo rimuove.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 23:27:29 +02:00
Adriano e20299548f fix(migrations): schema base 000 per DB vuoti, stamp head dopo create_all (#6)
upgrade head partiva dalla 001, che sa solo aggiungere colonne a tabelle
che assume gia' esistenti: su un DB davvero vuoto falliva subito. Nuova
000_base_schema.py (revision "000_base", down_revision None) crea lo
schema pre-001 esplicitamente (create_table, non Base.metadata.create_all,
che porterebbe gia' le colonne di 001-013 e farebbe fallire 001 su
"Duplicate column"). No-op se la tabella recipes esiste gia'
(sa.inspect(bind).has_table("recipes")). 001 ora ha down_revision =
"000_base".

setup.py: init-db e reset-db, dopo create_all/drop_all+create_all,
eseguono l'equivalente di "alembic stamp head" (nuovo _stamp_head(),
command.stamp con la config di src/backend/migrations/alembic.ini) cosi'
un DB nato da create_all non ritenta le migrazioni 001-013 al prossimo
deploy.

Test: catena upgrade head su SQLite vuoto, tabelle/colonne confrontate
con Base.metadata; DB con recipes gia' nello stato pre-001 -> upgrade
head non fallisce (000 no-op). _stamp_head si appoggia a
migrations/env.py, che costruisce una connessione propria da
settings.database_url (non modificabile dai test tramite l'engine
patchato di setup.py): nei test di setup.py e' quindi no-op via
monkeypatch, cosi' init-db/reset-db continuano a girare su SQLite senza
toccare un MySQL vero.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:49:38 +02:00
Adriano 805f41c757 fix(upload): tipo da magic bytes, non da Content-Type dichiarato (#36 punto 2)
Il Content-Type del client non era verificato: un file "x.html" dichiarato
image/png veniva salvato e poi servito per estensione come text/html,
eseguito dal browser. Nuova detect_file_type() (PNG/JPEG/GIF/WebP/PDF) in
files.py, usata anche dal logo in settings.py; l'estensione salvata segue
sempre il tipo rilevato, mai il nome file dichiarato dal client. Il logo
rifiuta SVG a prescindere (puo' portare script). Upload letti a blocchi da
1MB, interrotti appena superano il limite invece di bufferizzare tutto
prima di controllare la dimensione.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:48:52 +02:00
Adriano bfe66a5710 fix(production): ruolo e assegnazione stazione su apertura/avanzamento run (#24 backend)
api/routers/production.py: open/cycle/remeasure/pause/resume/close accettavano
qualunque utente autenticato (get_current_user). Nuova dipendenza locale
_require_run_operator (MeasurementTec o Supervisor o admin) sui sei endpoint
di ciclo vita; le letture (current, {run_id} GET) restano aperte a tutti gli
autenticati. Per pause/resume/close si aggiunge al controllo esistente delle
credenziali del supervisore in corpo, che riguarda un'altra persona (chi
autorizza), non chi opera.

services/production_service.py open_run: nessun controllo su recipe.active ne'
sull'assegnazione ricetta-stazione. Nuovo _require_recipe_assigned_to_station:
se la stazione ha almeno un'assegnazione (StationRecipeAssignment), la ricetta
deve esserci, altrimenti 409; una stazione senza alcuna assegnazione resta
libera - stessa regola del filtro in lettura di station_service.list_station_recipes
(join che senza righe non filtra nulla). Ricetta non active -> 409.

NON TOCCATO: src/frontend/flask_app/blueprints/measure.py (stesso filtro sul
lato Flask, lotto B2) e test_measure_station_filter.py - altro agente ci
lavora in parallelo sulla stessa cartella.

Test: src/backend/tests/test_production_runs.py, 6 nuovi test (ruolo su
open/pause, assegnazione rifiutata/accettata/libera, ricetta inattiva).
PROVA: uv run --extra server --extra dev pytest src/backend/tests/test_production_runs.py -q -> 34 passed

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:48:17 +02:00
Adriano 77d95fdcc4 fix(tasks): copy-on-write su modifica/cancellazione e ordine tolleranze (#4, #26)
#4 - api/routers/tasks.py: update_task, delete_task, create_subtask,
update_subtask, delete_subtask controllavano solo is_current, non
version_has_measurements: la versione con misure veniva modificata sul posto,
cambiando i limiti di quote gia' giudicate pass/fail. Nuovi helper
_task_for_write / _subtask_for_write riusano recipe_service.create_new_version
(lo stesso copy-on-write di create_task): se la versione ha misure creano una
nuova versione, ritrovano il task per order_index e la quota per marker_number
(chiavi che _copy_tasks_to_version preserva), e la scrittura va sulla copia.
La versione misurata resta intatta.
Trovato e corretto nello stesso punto: il task ritrovato nella copia non ha
.version precaricato (new_version.tasks viene da selectinload, che non popola
il backref figlio->genitore), e TaskResponse.recipe_id legge task.version.recipe_id
- senza assegnare esplicitamente candidate.version = new_version, update_task
va in MissingGreenlet (lazy load fuori da un await) invece di rispondere 200.

#26 - models/api/task.py: nuovo validate_tolerance_order (ltl<=lwl<=nominal<=
uwl<=utl sui soli campi valorizzati), usato come model_validator su
SubtaskCreate e richiamato dal router in update_subtask sui valori risultanti
(esistenti + parziali) prima del flush, perche' SubtaskUpdate non ha un insieme
fisso di campi da validare da solo.

Test: src/backend/tests/test_tasks.py, TestCopyOnWriteOnEdit (5) e
TestToleranceOrder (4); adattato test_update_subtask esistente (nominal 12.0
violava il nuovo ordine contro utl=10.5 invariato).
PROVA: uv run --extra server --extra dev pytest src/backend/tests/test_tasks.py -q -> 16 passed

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:48:08 +02:00
Adriano 3e99b64fc4 fix(users): roles/is_admin null -> 422, admin non si toglie da solo (#36 punto 4)
roles e is_admin sono NOT NULL nel DB ma Optional nello schema: {"roles":
null} o {"is_admin": null} passava la validazione Pydantic e falliva al
flush con un 500 non gestito. Ora 422 esplicito. Aggiunta anche la
protezione sull'ultimo admin: un admin non puo' togliersi is_admin da solo
(409) - unico caso raggiungibile via API dato che solo un admin puo'
chiamare l'endpoint, quindi rimuovere il flag a qualcun altro lascia sempre
il chiamante; il controllo sul conteggio resta comunque come difesa in
profondita'.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:45:43 +02:00
Adriano 403c923188 fix(ai): risposta AI malformata -> 502, non 400 (#36 punto 7)
json.JSONDecodeError e' un ValueError: KeyError/JSONDecodeError sulla
risposta del modello finivano nell'except ValueError del router (pensato
per i nostri errori di configurazione, es. API key mancante) e uscivano
come 400 invece di 502. Nuova AIResponseError dedicata in ai_service.py
per contenuto malformato o JSON non valido; recipes.py la traduce in 502,
toccato solo per questo except.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:43:42 +02:00
Adriano 65f6a4b18b feat(vision): immagini di riferimento non orfane, GET/DELETE (#38.5)
save_reference_image scrive il file dopo il flush riuscito; il router
lo cancella se il commit successivo fallisce. Aggiunti GET
/api/vision/reference-images?task_id= (lista, Maker) e DELETE
/api/vision/reference-images/{id} (cancella riga e file, Maker).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:43:35 +02:00
Adriano 5ca3dc14cd fix(logging): action troncata a 100, ip_address dal client IP proxy-aware (#36 punto 6)
action era action=f"{method} {path}" senza limite: un path lungo falliva
l'INSERT (String(100)), inghiottito dal try/except generico, e la riga di
audit spariva in silenzio. Troncata a 100 caratteri. ip_address usava
request.client.host, che dietro Nginx/Flask e' sempre il proxy: ora usa
RateLimitMiddleware._client_ip(request), lo stesso risolutore proxy-aware
del rate limiter (X-Forwarded-For con trusted_proxy_count).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:42:16 +02:00
Adriano 1c5292d1e0 fix(vision): execute controlla task_type, versione, ricetta attiva, produzione (#38.7)
execute_task rifiuta 422 se il task non è camera_measure, 409 se la
versione non è corrente o la ricetta non è attiva; production_run_id,
quando presente, è validato con measurement_service.validate_run_for_version
(stessa regola di una lettura manuale). Router: lot_number/serial_number
max_length=100, production_run_id gt=0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:41:22 +02:00
Adriano ac76c0a395 fix(auth): api_key condivisa fra postazioni, timing costante al login (#25)
login_user riusa l'api_key esistente invece di rigenerarla ad ogni
login: due postazioni collegate con lo stesso utente restavano
altrimenti disconnesse a vicenda. logout_user diventa no-op sulla key
a meno di all_sessions=True (nuovo parametro, passato da
POST /api/auth/logout?all_sessions=true nel router).

authenticate_user esegue comunque bcrypt.checkpw contro un hash fittizio
quando lo username non esiste, per non rivelare col tempo di risposta
se uno username è registrato.

PUT /api/users/{id}/password azzera l'api_key dell'utente dopo il
cambio, come già fa setup.py:634.

Non introdotto in questa issue: hashing o scadenza della api_key
(annotato come MANCA nel rapporto del lotto).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:40:05 +02:00
Adriano 1d5f24510c fix(measurements): export CSV ammette admin senza ruolo esplicito (#36 punto 3)
Il controllo ruolo di GET /api/measurements/export/csv ignorava is_admin:
un admin senza MeasurementTec/Metrologist assegnato prendeva 403, a
differenza di ogni altro endpoint che passa da require_role (ruolo o admin).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:39:10 +02:00
Adriano 78970c69b0 fix(setup): confronto costante, blocco dopo l'init, status minimale (#9)
_check_password confrontava la password con !=, ora hmac.compare_digest.

Nuova guardia _require_not_initialised: se esiste già un admin,
reset-db, seed, manage-user, change-user-password e init-db rispondono
403 "setup already completed", a meno di settings.setup_allow_after_init
(nuovo, env SETUP_ALLOW_AFTER_INIT, default False). create-admin,
list-users e toggle-user-active restano fuori dalla guardia.

GET /api/setup/status senza password valida restituisce solo
{"initialised": bool}; l'elenco tabelle solo con la password.

Adeguati i test esistenti che seedavano un admin e poi richiamavano un
endpoint ora guardato (test_setup.py, test_station_seed.py: bypassano
la guardia con setup_allow_after_init quando serve testare altro).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:38:09 +02:00
Adriano 9afe1e712b fix(vision): file non immagine e grafo malformato non escono più come 500 (#15)
Worker: Image.open().convert() in try/except (422 "image is not
decodable"); il grafo deve essere un dict (422 altrimenti); run_graph
distingue costruzione (GraphBuildError -> 422) da esecuzione (500 con
motivo). Router: verifica dei magic bytes (PNG/JPEG/BMP/TIFF/WebP/GIF)
prima di chiamare il worker.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:37:34 +02:00
Adriano e8a8aeed66 fix(files): un Maker non cancella statistics/reports/logo, GET ristretto (#21)
DELETE /api/files/{path} non aveva nessun legame con ricetta/proprietario:
un Maker poteva cancellare qualunque file sotto uploads, compresi i CSV di
statistica di produzioni altrui. Ora: statistics/, reports/, logos/ sono
403 sempre (il logo si cambia da PUT /api/settings/logo); un file di
ricetta (task.file_path, subtask.image_path, recipe.image_path) e' 409 se
referenziato da una versione non corrente o che ha gia' misurazioni - un
file non referenziato resta cancellabile. GET di statistics/* e reports/*
richiede Metrologist, Supervisor o admin (prima bastava essere autenticati).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:36:49 +02:00
Adriano e91ef739af fix(rate-limit): secchio login esteso a credenziali supervisore e setup (#8)
Il secchio "login" del rate limit valeva solo per /api/auth/login. Un
capoturno può ora essere attaccato a forza bruta anche via
/api/measurements/{id}/authorise e /api/production-runs/{id}/pause|
resume|close, senza mai toccare il bucket più permissivo "general".
_is_login_bucket_request confronta il path con un'espressione regolare,
non con ==, perché l'id in mezzo varia per richiesta.

Include anche la parte di #9 che riguarda questo stesso file: i path
sotto /api/setup/ entrano nello stesso secchio, indipendentemente dal
metodo (GET /api/setup/status accetta ora una password opzionale).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:34:42 +02:00
Adriano a330f2b340 fix(rate-limit): non fidarsi del primo hop di X-Forwarded-For (#7)
_client_ip usava il primo elemento di XFF, impostabile dal client. Ora
usa l'elemento a -trusted_proxy_count (nuova impostazione, default 1,
env TRUSTED_PROXY_COUNT), quello aggiunto dal proxy fidato più esterno.
Con trusted_proxy_count=0 XFF e X-Real-IP sono ignorati.

Aggiunta anche web_concurrency (env WEB_CONCURRENCY, default 1): i
bucket del rate limit sono per processo, con --workers N il limite
configurato viene diviso per N (arrotondato per eccesso, minimo 1) in
_effective_limit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:34:02 +02:00
Adriano 0dce2299d0 fix(csv): neutralizza injection di formule nei campi testuali export (#22)
lot_number, serial_number (export manuale e di produzione), station.code,
recipe.code, subtask.description finivano nel CSV cosi' come arrivavano:
un lotto '=HYPERLINK(...)' apriva un link vivo in Excel/LibreOffice. Nuova
neutralise_csv_cell() in production_export_service.py, importata da
measurements.py: se il campo (spazi iniziali esclusi) comincia con =, +, -,
@, TAB o CR antepone un apice, cosi' resta testo letterale. Applicata solo
ai campi testuali, mai ai numeri.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:33:32 +02:00
Adriano ea9da34899 fix(settings): valida csv_delimiter/decimal_separator/auto_logout_minutes (#23)
PUT /api/settings/ non validava nulla: un csv_delimiter vuoto o di piu'
caratteri arrivava intatto nel DB e faceva fallire csv.writer con TypeError
dentro close_run (500, run mai chiuso). Aggiunta validazione per chiave nota
(csv_delimiter 1 carattere fra , ; TAB |, csv_decimal_separator ',' o '.',
auto_logout_minutes vuoto o intero 1-480, company_name/company_logo_path
non vuoti) -> 422 sui valori fuori dominio. Chiavi non note passano senza
validazione (store generico).

Difesa in profondita' in production_export_service._csv_format: un
csv_delimiter gia' sporco nel DB (scritto prima di questa validazione) non
fa piu' fallire close_run, usa ';' e logga un WARNING.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:29:35 +02:00
Adriano 0bd95367bb fix(vision): limite di tipo e dimensione sui tre endpoint di upload
/execute, /preview e /reference-images leggevano l'immagine senza
limite di tipo o dimensione, e /reference-images scriveva su disco con
estensione .png fissa qualunque fosse il tipo reale. Riusa la
convenzione già esistente in files.py (ALLOWED_IMAGE_TYPES,
validate_file_size) invece di inventarne una seconda: tipo controllato
dall'header prima di leggere, dimensione controllata sul corpo letto,
un PDF non è qualcosa che un grafo di visione può misurare.
save_reference_image ora scrive con l'estensione del tipo reale
dell'upload.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
2026-08-16 19:41:14 +02:00
Adriano 533edc5be8 fix(vision): il grafo di visione sopravvive a copy-on-write e creazione
_copy_tasks_to_version copiava una lista esplicita di campi che ometteva
vision_json e vision_output: la prima modifica a una ricetta con misure
produceva una versione nuova con i task camera senza grafo. Stessa
famiglia di bug nei percorsi di creazione (POST /api/recipes/{id}/tasks,
POST /api/tasks/{id}/subtasks): i campi erano dichiarati negli schemi ma
mai passati ai costruttori ORM, quindi l'API rispondeva 200 senza
salvare nulla. TDD: test_vision_copy_on_write.py fallisce prima del fix,
passa dopo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
2026-08-16 19:36:18 +02:00
Adriano 3870cc3b0f feat(vision): immagini di riferimento, per comporre senza camera
VisionReferenceImage (migration 013, chained after 012_vision_results):
task_id, path, station_id, device_code, calibration_snapshot,
engine_version, note, expected_json, acquired_at. station_id/device_code/
calibration_snapshot/engine_version/expected_json stay nullable - nessun
endpoint di questo task li popola ancora, arrivano coi Piani 1b/2/4.

save_reference_image salva sotto settings.upload_path (non upload_dir,
come production_export_service). preview esegue il grafo e non salva
niente - comporre non e' misurare, lo asserisce il test.

POST /api/vision/reference-images e /api/vision/preview su require_maker
(non require_measurement_tec di execute): comporre e provare sono lavoro
del Maker, non misure.

Suite intera su Python 3.11.15 locale (uv run pytest -q, SQLite
in-memory per il backend): 1 failed, 373 passed, 4 skipped in 82.35s.
Il fallimento e' quello preesistente e non correlato di
test_no_first_party_script_calls_out (fabric-debug.js locale,
git-ignored) - 371 passed prima di questo task, +2 qui.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
2026-08-16 19:11:47 +02:00
Adriano ddf7788d77 feat(vision): il server esegue tramite worker e salva misure vere
Aggiunge POST /api/vision/execute: route l'immagine al worker di visione,
mappa le uscite del grafo sulle quote e le salva con save_measurement -
stesso verdetto, stesso gate del fuori tolleranza di ogni altra misura.
Nuova tabella vision_results (una riga per acquisizione, non per quota) e
input_method 'camera' su measurements.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
2026-08-16 18:40:12 +02:00
Adriano Dal Pastro 98b1c5ae85 feat(measure): l'operatore entra nella sequenza, non in un elenco
Punto 10. Sceglere una ricetta apriva la lista completa dei task e chiedeva
all'operatore di decidere da dove cominciare prima ancora di aver fatto
qualcosa. Ora AVVIA IN SEQUENZA porta dentro il primo task; la lista resta,
un livello sotto, per quando serve vederla tutta o tornare indietro.

- nuova rotta /measure/start/<ricetta>: memorizza lotto e seriale (prima lo
  faceva la lista, che ora si salta), verifica la tracciabilità obbligatoria
  e apre il primo task
- GET /api/measurements/task-progress: quante quote ha già preso ogni task di
  misura, contate per quota e non per tentativo, delimitate dalla produzione
  aperta o, fuori produzione, dall'operatore
- la lista distingue «Incompiuto 1/3» da «Completato 3/3»: un task lasciato a
  metà non somigliava più a uno mai aperto
- «Visualizza singolo TASK» non è più riservato al Maker — dire «incompiuto»
  senza dare la strada per tornarci sarebbe una lamentela, non una funzione
- nel task «Riepilogo» diventa «Completato» e si apre solo quando il ciclo è
  chiuso: altrimenti sarebbe la scorciatoia che rende facoltative le quote
- «Fine ciclo misura» si vede da subito, spento, e dice quante quote mancano;
  prima compariva a task già finito, quando non serviva più saperlo

Il pulsante verde del footer diventa «Task successivo»: due bottoni con la
stessa parola addosso sono uno di troppo.

Test: 334 (erano 309). Il side effect dei mock del client Flask ora risponde
per endpoint invece che per turno — una lista posizionale si rompe appena una
pagina fa una domanda in più al server, che è come crescono tutte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 21:31:42 +00:00
Adriano Dal Pastro e2aae88858 feat(measure): il fuori tolleranza si autorizza, non si chiude
Il punto 5 c'era a meta': la schermata apriva il modale del capoturno e bloccava
l'avanzamento automatico, ma il modale si chiudeva con Annulla o con un click
sullo sfondo, la misura era gia' salvata, e `pendingAdvance` veniva impostato e
non letto da nessuno. Era una conferma, non uno sbarramento.

Ora l'autorizzazione finisce sulla misura: due colonne, chi ha autorizzato e
quando. Finche' una quota e' fuori tolleranza e nessuno l'ha autorizzata, il
server rifiuta la misura della quota successiva e la chiusura del ciclo. Non e'
la schermata a impedirlo: la schermata risparmia solo il viaggio.

Rimisurare la stessa quota resta possibile — il calibro scivola, il pezzo si
riposiziona — e una seconda lettura in tolleranza libera il blocco, perche' conta
l'ultima lettura di ogni quota. Quante volte si possa riprovare e' il punto 4, e
tutte le letture restano comunque a registro. Un warning non blocca: fuori dai
limiti di attenzione ma dentro la tolleranza e' dentro la tolleranza.

Ricaricare la pagina era il modo piu' semplice per scavalcare il vecchio gate.
Non lo e' piu': la schermata chiede al server, all'apertura, se una quota sta
aspettando, e si ritrova davanti lo stesso blocco.

Sparisce /validate-supervisor, che verificava le credenziali e buttava via la
risposta. Al suo posto un endpoint che le credenziali le usa per scrivere
l'approvazione dove serve. Il controllo del capoturno si sposta in auth_service,
accanto al resto delle credenziali: fermo linea, chiusura e fuori tolleranza
fanno la stessa domanda, e solo una delle tre riguarda la produzione.

Il file di statistica guadagna authorised_by e authorised_at: mostrava il
fallimento e non la decisione, che e' la meta' che un auditor chiede.

Migrazione 010: due colonne nullable sulle misure. Le righe esistenti restano
nulle — retrodatare un'autorizzazione mai avvenuta sarebbe inventarsi un record
di audit, e quelle produzioni sono chiuse da un pezzo.

Test: +18 (309). Coprono il rifiuto della quota successiva, la rimisura ammessa,
il rilascio del blocco con una lettura buona, il ciclo che non si chiude, il
capoturno registrato sulla misura, le credenziali sbagliate e chi capoturno non
e', il file di statistica, e il fatto che una produzione chiusa non blocchi la
successiva. Aggiunto uploads/statistics/ al gitignore: i test che chiudono una
produzione scrivevano nel repository.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 20:54:24 +00:00