Commit Graph

51 Commits

Author SHA1 Message Date
Adriano a2f357b3d3 fix(config): rifiuta i segreti di default fuori da dev/test (#33, parte A2)
Nuova impostazione app_env (env APP_ENV, default "dev"). All'istanza di
Settings, se app_env non è "dev"/"test" e SERVER_SECRET_KEY o
DB_PASSWORD sono ancora il valore committato, RuntimeError che nomina
la variabile. Dev e test continuano a partire senza .env: il default
"dev" non fa mai scattare la guardia.

Le parti frontend (flask_app/config.py) e deploy (docker-compose.yml)
di questa issue sono degli altri lotti (B2, C2).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:41:28 +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 302903a8a7 fix(recipe): task di default per il disegno e' task_type=drawing (#36 punto 5)
update_current_version creava il task di default (nessun task ancora sulla
versione) senza task_type, quindi finiva "note" invece di "drawing" come
fa create_new_version alla riga ~293. Aggiunta la riga mancante.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:39:50 +02:00
Adriano b15037851b fix(vision): /run del worker non blocca più l'event loop, timeout e semaforo (#34)
Decodifica + run_graph girano in asyncio.to_thread sotto asyncio.wait_for
(VISION_RUN_TIMEOUT_S, default 100s, sotto i 120s del backend) -> 504 del
worker allo scadere. /health resta leggero e risponde durante un calcolo
in corso. asyncio.Semaphore(VISION_MAX_CONCURRENCY, default 2): oltre ->
503 "worker busy". vision_service._call_worker inoltra 422/503/504 del
worker con il loro status e motivo invece di appiattirli su 502.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:39:28 +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 3f70f99a40 fix(async): chiamate bloccanti in asyncio.to_thread (#36 punto 1)
kaleido (pio.to_image), WeasyPrint (write_pdf), pdfplumber (extract_text_from_pdf)
e la scrittura del CSV di produzione giravano sincrone dentro handler async:
un PDF pesante o un run grande fermava il worker per tutte le altre richieste.
Ora avvolte in await asyncio.to_thread(...) in report_service.py,
ai_service.py e production_export_service.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:38:28 +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 20957e517d fix(vision): NaN/inf/bool di uno strumento non diventano più una quota (#16)
runner.run_graph esclude bool e NaN/inf dagli output numerici, con il
motivo in RunOutcome.invalid_outputs. vision_service tratta una quota
mappata con valore None/assente/non finito come "non prodotta" (422),
invece di arrivare a float(None) e sollevare TypeError.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:35:53 +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 67392a946c fix(vision): worker down o in timeout non esce più come 500 nudo (#14)
_call_worker distingue httpx.TimeoutException (504) dal resto dei
TransportError/HTTPError (502, RemoteProtocolError incluso). Un 422 del
worker si propaga come 422 con il motivo del worker, non più 502 generico.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:34:06 +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 7249cb8a8c fix(spc): Cp/Cpk usa sigma within (MRbar/1.128), Pp/Ppk la stdev campionaria (#19)
Prima Cp/Cpk usava stdev campionaria e Pp/Ppk la population stdev: stesso
indice due volte, proporzionale. Ora Cp/Cpk (capability a breve termine)
usa sigma_within dalla moving range dei valori consecutivi in ordine
cronologico; Pp/Ppk (performance complessiva) resta sulla stdev campionaria
(n-1). MRbar == 0 -> Cp/Cpk None. Stessa sigma_within per UCL/LCL della
carta di controllo a valori individuali (mean +/- 2.66*MRbar), non piu la
stdev globale che allargava i limiti e nascondeva la deriva.

Aggiunto campo opzionale std_dev_within a CapabilityData.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:27:06 +02:00
Adriano f20b92bafb test(vision): la suite non lascia piu' immagini nel repository
Il file di test dei limiti di upload scriveva un jpeg vero in
uploads/vision/reference/ a ogni esecuzione: gli mancava la fixture di
dirottamento che test_vision_reference.py ha gia'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
2026-08-16 20:28:35 +02:00
Adriano 85d7976f4e docs(vision): commenta perché camera resta fuori dal pattern client
MeasurementCreate.input_method accetta solo usb_caliper|manual anche se
l'enum ORM e il database permettono ormai camera. È voluto: una misura
da camera deve nascere in vision_service.execute_task, mai da un client
che posta JSON direttamente - non c'è dietro né immagine, né grafo, né
engine_version. Commento aggiunto perché non venga "corretto" tornando
alla lettera dell'enum.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
2026-08-16 19:49:21 +02:00
Adriano a72e3f11af test(vision): chiude il punto cieco del confine, "Acquire" era un fantasma
FORBIDDEN confrontava solo il primo segmento del path importato: "from
src.vision.runner import ..." dava radice "src", mai vietata (il
backend importa se stesso di continuo), quindi il test non l'avrebbe
mai vista. Confermato manualmente: la logica vecchia su quell'import
restituisce un insieme vuoto di violazioni. Ora si confrontano i path
puntati per intero contro "src.vision"/"src.vision_worker" come
prefissi, non solo la prima radice.

"Acquire" in FORBIDDEN era un nome di classe/SDK (Balluff "mvIMPACT
Acquire"), mai una radice di import: non poteva mai far scattare nulla.
vs-camera importa sotto lo stesso namespace visionsuite di vs-core
(vendor/visionsuite/packages/vs-camera/pyproject.toml: "il codice si
importa come visionsuite.camera...."), già coperto da "visionsuite":
non c'era una radice separata da aggiungere, quindi è stato tolto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
2026-08-16 19:45:00 +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 38ef0587f5 fix(vision): il guasto del worker resta suo, il gate anticipa il worker
I2: _call_worker lasciava propagare httpx.ConnectError e ReadTimeout
(worker fermo o job oltre i 120s), senza handler in main.py: l'operatore
vedeva un 500 nudo. Ora ConnectError->502, ReadTimeout->504, ciascuno con
un messaggio che nomina il guasto. La forma della risposta è validata
alla frontiera: outputs/failures/engine_version mancanti o un body non-
dict non fanno più KeyError, rispondono 502 "malformed".

M3: il gate del fuori tolleranza (pending_authorisation) girava dopo
_call_worker: un operatore bloccato bruciava un'intera esecuzione di
visione prima di ricevere il 409. Spostato prima della chiamata al
worker; le quote necessarie erano già caricate prima, nessun'altra query
serviva.

Copertura: test_vision_worker_transport.py (nuovo) per i quattro casi di
guasto/malformazione; test_vision_execute.py aggiunge
test_a_pending_fail_blocks_before_the_worker_is_called, rossa prima dello
spostamento del gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
2026-08-16 19:38:56 +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 398af016f8 test(vision): il confine fra server e VisionSuite è sorvegliato 2026-08-16 19:03:16 +02:00
Adriano b86eeada86 fix(vision): il gate del fuori tolleranza vale per l'acquisizione, non per la quota
Un'acquisizione camera produce N quote nello stesso istante: non c'e "andare
avanti" fra l'una e l'altra, quindi il gate sequenziale di save_measurement
non deve scattare dentro il lotto. execute_task valuta pending_authorisation
una sola volta per l'intera acquisizione (bloccando solo se il fail pendente
non appartiene alle quote di questo task) e passa enforce_tolerance_gate=False
a ogni save_measurement del lotto. Query delle quote ordinata per
marker_number, cosi l'esito non dipende dall'ordine di ritorno di SQLite/MySQL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
2026-08-16 18:58:46 +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 b578ac9e1b feat(vision): il grafo vive sul task, la quota nomina la sua uscita 2026-08-16 17:10:20 +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
Adriano Dal Pastro 5aa3d595ad feat(recipes): la ricetta decide cosa pretendere e come si misura
Tre cose che prima non erano di nessuno diventano regole della ricetta, decise da
chi la scrive e fatte valere dal server.

Punto 8 — tracciabilita' obbligatoria. Lotto e seriale si dichiarano obbligatori
sulla ricetta. L'operatore li inserisce alla selezione, dove il pulsante non si
attiva finche' mancano, e la stessa regola vale sulla lista task, raggiungibile
anche per link diretto, e sul barcode: uno sbarramento che dura solo finche'
qualcuno prende in mano il lettore non e' uno sbarramento. La produzione non si
apre e la misura non si salva senza cio' che la ricetta pretende, perche' un
valore senza il suo lotto non e' riconducibile a niente, e accorgersene dopo
significa accorgersene tardi.

Punto 9 — inserimento manuale. Una ricetta puo' vietare i valori digitati, ed e'
il valore predefinito: il calibro e' lo strumento, digitare e' cio' che va
concesso. Dove e' vietato il tastierino non viene disegnato (non nascosto con i
CSS: il markup nascosto e' markup che si puo' rimostrare) e restano correzione e
conferma, perche' una lettura sbagliata va cancellata. Il controllo vero e' sul
server: una regola che vive solo nel frontend e' un consiglio.

Migliorato al passaggio il riconoscimento del calibro. Contava solo la raffica di
cifre, cosi' una lettura corta come "9.5" — tre battute — finiva registrata come
digitata a mano; ora conta anche l'Invio che il wedge manda dentro la stessa
raffica. Senza questa correzione il divieto avrebbe respinto misure legittime.

Punto 11 — formattazione delle descrizioni. A capo e grassetto sopravvivono: chi
scrive le ricette incolla dal PDF della scheda tecnica e il testo arrivava
appiattito, da risistemare a mano ogni volta. Nessun HTML viene accettato o
salvato — il testo viene escapato e gli unici tag nel risultato sono quelli
prodotti dal renderer. La sanificazione e' questa: non c'e' niente da sanificare
perche' non si accetta niente. Le stesse due regole in Jinja e in JS, cosi' una
descrizione si legge uguale ovunque. E la descrizione ora si vede anche in
esecuzione: era scritta per l'operatore e la vedeva solo chi la scriveva.

Migrazione 009: tre colonne sulla ricetta. Le due di tracciabilita' partono
false, che e' il comportamento di oggi; l'inserimento manuale parte *vero* sulle
ricette gia' esistenti — il default della colonna e' falso, quindi le ricette
nuove sono solo-calibro, ma spegnerlo d'ufficio su quelle in uso fermerebbe una
linea alla misura successiva. Chi possiede la ricetta lo decide dall'editor.

Test: +26 (291). Coprono il rifiuto sul server per lotto, seriale e valore
digitato, il calibro sempre ammesso, le regole che sopravvivono alla nuova
versione, il tastierino assente in pagina, l'Avvia sbarrato, e il renderer delle
descrizioni compreso il caso in cui si prova a farci passare un tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 20:27:19 +00:00
Adriano Dal Pastro bde8fafd77 feat(production): la misura torna a cercare l'operatore
Il conto alla rovescia non si ferma piu' a zero. Oltre la scadenza continua
nell'altro senso e dice da quanto la linea e' in ritardo, in rosso, su tutte le
schermate del percorso di misura: lista task, riepilogo, scelta ricetta. Un
ritardo va letto, non dedotto. Allo scadere dell'intervallo la misura si
ripropone da sola: cicalino, un avviso di cinque secondi, e l'operatore torna al
primo task di misura da qualunque schermata si trovi.

Un ciclo copre tutti i task di misura della ricetta, quindi solo l'ultimo lo
chiude e fa ripartire l'intervallo. Farlo ripartire al primo avrebbe fatto
misurare una ricetta con tre task di misura tre volte piu' spesso di come e'
configurata. E' il server a decidere quale task chiude il ciclo, perche' e' lui
a conoscere la sequenza: la lista dei task di misura viaggia con la produzione
(measurement_task_ids), che e' anche cio' che permette a una schermata qualsiasi
di sapere dove riportare l'operatore.

Aggiunta la rimisura: si gira il pezzo e si misura di nuovo dentro lo stesso
ciclo, senza chiudere niente e senza guadagnare tempo sulla scadenza. Le due
letture restano entrambe in statistica, che e' il motivo per cui si prendono.

Chi sta gia' misurando quando l'intervallo scade e' in ritardo, non perso: la
banda diventa rossa e lo si lascia lavorare. Portarlo altrove a meta' ciclo
cancellerebbe quote che ha davanti agli occhi senza guadagnare nulla.

Sistemato anche il pulsante "Conferma ciclo" dell'overlay: alzava una bandierina
locale e basta, quindi il ciclo non veniva mai registrato sul server da quella
strada. Ora passa da confirmCycle come il pulsante della barra.

Migrazione 008: il registro eventi della produzione impara task_measured e
remeasure, e la colonna task_id — un task_measured che non dice quale task non
registra niente di utile. I valori nuovi entrano tutti insieme perche' allargare
una enum MySQL riscrive la tabella, stesso ragionamento dei tipi di task in 007.
Verificata su SQLite (batch mode) e in MySQL con --sql.

La logica dell'orologio — come si legge un ritardo, quando suona, dove sta la
misura — vive in un solo posto (production-clock.js) e la schermata di misura la
usa invece di riscriverla.

Test: +10 (265). Coprono il ciclo che non riparte a meta', la rimisura che non
sposta la scadenza, il rifiuto a linea ferma, la sequenza dei task di misura
esposta dalla produzione, i proxy Flask e la validita' JS della lista task.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:31:51 +00:00
Adriano Dal Pastro 6fbff2fe76 feat(tasks): il tipo di un task si dichiara, non si deduce
Punto 2 del documento modifiche del 28/07, prima richiesta di Menoncin.

Il sistema distingueva un task di misura da uno documentale deducendolo: se aveva
quote era una misura, altrimenti una nota. Conseguenza: un task di misura a cui le
quote non erano ancora state inserite veniva trattato come nota, e il sistema si
comportava in modo diverso a seconda di quanto fosse completa la ricetta.

Nuovo campo task_type su recipe_tasks (migrazione 007) con nota, misura e disegno,
piu' xf_compare e camera_measure gia' nell'enum: allargare un enum MySQL piu' avanti
e' un ALTER su tabella viva, e non costa nulla prevederli adesso.

Il backfill riproduce la classificazione che era a schermo, cosi' nessuna ricetta
cambia comportamento all'aggiornamento: i task con quote diventano 'measure'; quelli
senza quote ma con un disegno allegato diventano 'drawing' e non 'note', perche' e'
gia' cio' che mostravano - chiamarli note sarebbe stato l'unico punto in cui questa
migrazione cambiava le carte in tavola.

Il tipo viene copiato esplicitamente nel copy-on-write del versioning: una nuova
versione che lo perdesse riclassificherebbe in silenzio tutti i task alla prima
modifica di una ricetta. Il task "Technical Drawing" creato d'ufficio quando si
carica un disegno su una ricetta senza task nasce come 'drawing'.

Lato operatore decide ora il tipo, non il conteggio delle quote: fermo linea, fine
produzione, avvio produzione e la barra di produzione seguono il tipo. Dove servono
davvero delle quote da mostrare - elenco marker, tastierino, fine ciclo misura -
resta anche il controllo che ce ne sia almeno una, e un task di misura ancora privo
di quote lo dichiara invece di somigliare a una nota. Nella lista task compare il
tipo, cosi' si vede prima di aprire.

La 007 e' stata eseguita su SQLite usa e getta con tre righe costruite apposta - una
con quote, una col solo disegno, una nota secca - e il backfill le classifica come
atteso. Il backfill girera' sui dati reali del cliente, provarlo a mano non bastava.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:15:18 +00:00
Adriano Dal Pastro 7bc3c1f938 feat(production): fermo linea e fine produzione fanno finalmente qualcosa
Punto 6 del documento modifiche del 28/07. I due pulsanti esistevano, chiedevano
correttamente l'autorizzazione del capoturno, e poi ricadevano su un commento:
"handled by GAIA integration (future)". E' la riga di apertura del documento.

Ora agiscono sulla produzione aperta: fermo linea sospende e congela il conto alla
rovescia, la ripresa lo fa ripartire restituendo il tempo del fermo, fine
produzione chiude ed emette il file di statistica. Le credenziali del capoturno
vanno direttamente all'endpoint, che le verifica e registra sulla traccia della
produzione chi ha autorizzato cosa: validarle a parte non lascerebbe traccia, e
sarebbe un giro in piu'.

In interfaccia il pulsante di fermo diventa "Riprendi" quando la linea e' ferma, e
una banda dichiara lo stato con il valore congelato a video: una linea ferma che
sembra in marcia e' il modo in cui si salta un intervallo di misura senza
accorgersene.

Il file di statistica ha richiesto di legare le misure alla produzione (migrazione
006): Measurement conosceva solo versione, lotto e seriale, nessuno dei quali
separa una produzione dalla successiva sulla stessa ricetta e sullo stesso lotto,
quindi "le misure dell'intera produzione" non era una query. Il CSV rispetta i
separatori configurati in system_settings e porta con se' i limiti di tolleranza:
senza quelli un esito pass/fail non e' piu' ricalcolabile dal file a distanza di
anni, che e' il senso di un documento per audit. Le misure esportate vengono
marcate synced_to_csv. Se la produzione non ha misure non viene scritto nulla: un
file vuoto sarebbe rumore nella cartella, non evidenza.

L'invio a GAIA resta assente e non abbozzato, con il punto d'innesto dichiarato in
close_run: una chiamata vuota che sembra collegata e' peggio di niente. Tutto il
resto del punto 6 non dipende dal gestionale e funziona adesso.

Anche la 006 e' stata eseguita su SQLite usa e getta prima di essere considerata
buona, e anche qui la prova ha trovato un difetto: aggiungere una colonna con
foreign key fa emettere ad alembic un ALTER di vincolo, che SQLite rifiuta. Ora usa
batch_alter_table con il vincolo nominato, che su MySQL resta un ALTER normale e su
SQLite ricostruisce la tabella - cosi' la chiave esterna non va persa per far
contento il dialetto dei test.

Corretto anche il proxy di salvataggio misura, che chiamava /api/measurements senza
slash finale e pagava un redirect 307 a ogni singola misura.

Traduzioni: pybabel aveva di nuovo indovinato sette voci marcandole fuzzy, e in
italiano "Linea ferma" era diventato "Lingua Preferita" su una banda di sicurezza.
Tradotte per esteso in IT ed EN e tolti i flag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:32:43 +00:00
Adriano Dal Pastro 55e5e0153f feat(production): dai una vita propria alla produzione, lato server
Punto 1 del documento modifiche del 28/07, il prerequisito su cui poggiano i
punti 3, 4 e 6.

Lo stato di una produzione viveva dentro una pagina del browser: timer, conteggio
cicli e flag "produzione avviata" erano variabili Alpine di task_execute.html, e
la navigazione fra task e' un ricaricamento completo, quindi cambiando task si
perdeva tutto. Da qui il loop di misura che non reggeva, il fermo linea che non
aveva nulla da fermare e l'assenza di storico.

Nuove tabelle production_runs e production_events (migrazione 005), endpoint REST
senza stato in memoria di processo - con un'app di stazione installata su ogni PC
il database e' l'unico posto condiviso - e il frontend che legge lo stato
all'apertura invece di tenerlo in memoria.

Tre scelte di modello:

- la scadenza e' un timestamp assoluto (next_measurement_at), non un contatore:
  il countdown si ricalcola da li' a ogni caricamento, e lasciarne andare la
  differenza sotto zero dara' gratis il contatore del ritardo del punto 3. Al
  client vanno i secondi gia' calcolati, non il timestamp: un datetime naive
  verrebbe letto nel fuso del browser e il conto sarebbe sfasato dell'offset UTC;
- l'intervallo di misura e' copiato sulla produzione, non referenziato: modificare
  la ricetta a produzione avviata non deve spostare una scadenza in corso;
- active_station_id rispecchia la stazione finche' la produzione e' aperta e va a
  NULL alla chiusura. Con un vincolo unico sopra, "una stazione = una produzione
  aperta" e' una garanzia del database e non un controllo soggetto a race; i NULL
  non collidono, quindi le produzioni chiuse si accumulano senza disturbo.

Il fermo linea congela il conto alla rovescia e alla ripresa la scadenza viene
traslata della durata del fermo, non ricalcolata: un fermo non regala ne' toglie
tempo all'operatore. L'autorizzazione del capoturno passa da authenticate_user e
non da un login, che rigenererebbe la sua API key buttando giu' la sessione che ha
aperta altrove.

La migrazione e' stata eseguita davvero, non solo scritta, su uno SQLite usa e
getta: upgrade e downgrade girano e le colonne coincidono con i modelli. La prova
ha trovato un difetto - create_unique_constraint dopo create_table e' un ALTER,
che SQLite rifiuta - ora il vincolo e' dichiarato dentro create_table.

Fuori da questo commit, per stare nei confini del punto 1: l'API espone gia'
pause, resume e close, ma i pulsanti fermo linea e fine produzione restano da
collegare (punto 6), e il rientro forzato sulla misura allo scadere e' il punto 3.

Corretti due difetti trovati strada facendo: env.py non importava ne' Station ne'
ProductionRun, quindi l'autogenerate di Alembic era gia' cieco sulle stazioni; e
task_execute.html, lo schermo con piu' JavaScript dell'applicazione, non era
coperto dal test di sintassi. Aggiungerlo ha richiesto di correggere l'helper, che
validava le espressioni Alpine solo come espressione singola e bocciava
@click="a = false; b = true", forma che Alpine accetta: ora prova entrambe le
letture e fallisce solo se cadono tutte e due.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:20:43 +00:00
Adriano Dal Pastro a159eb2b8b feat(stations): ricette in lista, reset per riga e cambio stazione al volo
Punto 7 del documento modifiche del 28/07, terza richiesta di Menoncin.

GET /api/stations ora usa StationWithRecipesResponse, che era gia' definita nello
schema senza che nessun endpoint la usasse. Non costa query in piu':
Station.assignments e' gia' selectin-loaded. In lista compaiono le sole ricette
attive, coerente con cio' che l'operatore vede davvero a quella stazione.

DELETE /api/stations/{id}/recipes azzera le assegnazioni di una stazione senza
eliminarla, cosi' si riassegna da capo. E' idempotente: azzerare una stazione gia'
vuota risponde removed=0, non 404. In tabella e' un pulsante per riga, con
conferma perche' l'azione e' distruttiva.

Il cambio stazione al volo (?station=CODE, override in sessione) serve al
collaudo: senza, provare N stazioni richiede N PC. E' dietro il flag
STATION_SWITCH_ENABLED, default 0. Non e' legato al ruolo admin perche'
/measure/select richiede MeasurementTec, e gatearlo su is_admin avrebbe escluso
proprio chi fa il collaudo. In fabbrica l'identita' della stazione viene
dall'installazione locale, e misurare contro le ricette di un'altra stazione
romperebbe in silenzio la tracciabilita': per questo il codice richiesto e'
validato prima di essere memorizzato - un refuso lascia l'operatore sulla
stazione configurata con un messaggio, invece di incastrarlo su una stazione
inesistente - e quando l'override e' attivo la pagina lo dichiara, con il codice
configurato accanto e un link per ripristinare.

Le traduzioni: pybabel update aveva indovinato dieci voci da stringhe simili
marcandole fuzzy, in entrambi i cataloghi. "Azzera Stazione" era diventato "Crea
Stazione", l'opposto, su un dialogo distruttivo. Non si vedeva perche' la
compilazione salta le fuzzy e ricade sul sorgente italiano, ma sarebbe emerso al
primo build con --use-fuzzy. Tradotte per esteso le 17 stringhe nuove in IT ed EN
e tolti i flag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:42:17 +00:00
Adriano Dal Pastro 28ee44bcf1 feat(client): includi librerie e font in locale per il funzionamento offline
Punto 12 del documento modifiche del 28/07. L'installazione a Trafilo e'
on-premise su rete di produzione isolata: finche' Alpine.js, Plotly, PDF.js,
Fabric.js e i Google Fonts venivano scaricati da CDN a ogni apertura di pagina,
senza internet l'interfaccia non partiva affatto (Alpine governa tutta la UI).

Le librerie passano in static/vendor/ con la versione nel nome del file, e la
CSP del backend si chiude a solo-origine. Il worker di PDF.js e' ripuntato in
tutti e quattro i template che lo usano: ripuntando solo lo script principale la
libreria si carica in locale ma il worker continua a cercare internet, e sembra
funzionare finche' non si apre un disegno.

Alpine e' pinnato a 3.15.12, cioe' la versione a cui "3.x.x" risolveva oggi: il
congelamento non cambia il comportamento di quanto e' gia' in esercizio, e toglie
il fatto che l'applicazione cambiasse da sola a ogni rilascio degli autori -
rilevante per le evidenze ISO 9001 / IATF 16949.

Inter e JetBrains Mono sono font variabili: un solo woff2 per subset copre tutti
i pesi, dove Google serviva lo stesso file sotto otto URL. Tenuti i soli subset
latin e latin-ext, che coprono per intero italiano e inglese.

Provenienza, versioni e impronte SHA-256 in static/vendor/VERSIONS.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:41:57 +00:00
Adriano 2a566321f3 fix(migrations): riparenta input_duration dopo 003_measurement_interval
Il rebase aveva creato due head Alembic (003_input_duration e
003_measurement_interval entrambi figli di 002): upgrade head all'avvio
del server sarebbe fallito. Rinominata la revision in 004_input_duration
con down_revision 003_measurement_interval — catena di nuovo lineare.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 14:45:12 +02:00
Adriano 3f03ec8ab1 feat(measure): memorizza tempo di inserimento misura (input_duration_ms)
Colonna nullable su measurements + migration 003, campo opzionale negli
schemi API (create singola e batch), passthrough nel proxy Flask e timer
lato client in task_execute: parte all'attivazione del subtask, si
resetta su auto-advance e navigazione manuale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 14:42:24 +02:00
Adriano Dal Pastro fd571c479e fix: code review — security fixes, dedup, cleanup
- setup: validate roles against users.VALID_ROLES (Supervisor was missing)
- files: fix path traversal prefix-match edge case (is_relative_to),
  dedupe path validation into resolve_upload_path(), use logging not print
- measurements: extract shared _build_measurement_filters() helper
- client app: prevent open redirect via Referer on /set-language
- maker: guard resp.json() in parse-technical-sheet proxy
- measure/maker: extract shared file proxy into services/file_proxy.py
- measure: localize supervisor validation error messages
- annotation-editor: remove global keydown listener in destroy()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 12:16:29 +00:00
Adriano Dal Pastro 6ff78b2150 feat(roles): add dedicated Supervisor (capoturno) role
Until now the out-of-tolerance authorization ("Autorizzazione capoturno")
required is_admin, conflating shift-leader authority with full system
administration. Introduce a combinable Supervisor role so a capoturno can
authorize overrides without admin privileges (roadmap D-0.8 / Phase 2).

Backend:
- users router: add "Supervisor" to the allowed roles (centralized as
  VALID_ROLES, deduplicating the create/update validation).
- middleware: add require_supervisor dependency (admins still bypass).

Frontend:
- measure.validate_supervisor: authorize users with the Supervisor role OR
  is_admin (was is_admin only).
- admin/users: Supervisor checkbox + orange role badge.
- profile: explicit Supervisor badge styling.
- EN translation for the new role label.

roles is a free JSON column, so no migration is required.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 09:02:24 +00:00
Adriano Dal Pastro a7254d8932 fix(tasks): eager-load version in reorder endpoint to fix MissingGreenlet
The reorder_tasks endpoint loaded RecipeTask with selectinload(subtasks)
only. Serializing via TaskResponse reads RecipeTask.recipe_id, a @property
that traverses the version relationship, triggering a lazy load outside the
async greenlet context -> MissingGreenlet error.

Add selectinload(RecipeTask.version), matching the pattern already used in
the get-task flow. Fixes the previously order-dependent test_reorder_tasks
failure; full suite now 179 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 07:50:58 +00:00
Adriano Dal Pastro d2bf0b5828 feat(maker): AI parsing schede tecniche PDF via OpenRouter
- Nuovo servizio ai_service.py: estrae testo da PDF (pdfplumber) + analisi AI (OpenRouter)
- Endpoint POST /api/recipes/{id}/parse-technical-sheet con validazione file
- UI: bottone "Importa da PDF" nel task editor con modale upload + preview editabile
- Task suggeriti modificabili/rimovibili prima della creazione bulk
- Config: OPENROUTER_API_KEY e OPENROUTER_MODEL in .env
- Dipendenze: pdfplumber + httpx aggiunti a server deps

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 11:01:00 +00:00
Adriano Dal Pastro 0748ce9b1e feat: timer intervallo misura su ricetta + auto-logout per inattività
- Recipe: nuovo campo measurement_interval_minutes (migration 003) con UI nel recipe editor
- Auto-logout: impostazione di sistema configurabile da admin/settings, timer inattività JS
  con warning modale 60s prima del logout, tracking eventi utente throttled
- Navbar: aggiunto link "Impostazioni" per admin (desktop + mobile)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:36:02 +00:00
Adriano Dal Pastro 6772e3166a fix(setup,ui): risolve errori setup + logo SVG theme-aware
- setup: corregge path templates (sibling di api/, non figlio) e firma TemplateResponse
- setup: invalida sessione client dopo operazioni che ruotano l'API key (init DB, creazione admin, cambio password, attivazione utente)
- api_client: include il nome del campo nei messaggi di errore 422 (FastAPI validation)
- ui: nuovo componente _app_logo.html (SVG inline theme-aware) usato su login e navbar
2026-05-20 07:42:49 +00:00
Adriano 182aa9fa9a feat(ui): recipe thumbnail su Select Recipe + fit (object-contain) su Gestione Ricette
- /measure/select: aggiunta miniatura 64×64 accanto al nome ricetta
  (object-contain per mantenere l'immagine intera, non croppata).
  Lo schema backend RecipeSummary esponeva solo id/code/name/active:
  aggiunti image_path e description così la card può rendere thumb
  e sottotitolo.
- /maker/recipes: la miniatura passa da object-cover (crop) a
  object-contain (fit), per visualizzare l'intera immagine della
  ricetta senza tagli.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:24:36 +02:00
Adriano fb96bad000 fix(tests): align stale tests with current API + bypass identity-map cache
- test_recipes: split update test into in-place vs copy-on-write paths
  (PUT now updates in-place when current version has no measurements).
  Added _seed_measurement() helper to force copy-on-write where needed.
- test_tasks::reorder: expunge_all() between POST and GET so the shared
  test session re-fetches RecipeVersion.tasks instead of returning the
  cached collection. Prod uses one session per request, so identity-map
  staleness only affects tests.
- test_measure::save_measurement_proxy: payload aligned to current API
  (version_id is required; pass_fail/deviation are computed server-side).

All 179 tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:07:32 +02:00
Adriano 85a00dea1b fix(api): allow MeasurementTec to list measurements for task_complete
GET /api/measurements required Metrologist, but the operator workflow
calls it from task_complete.html to render the post-recipe riepilogo —
silently empty for every non-Metrologist user. Open to any authenticated
user; payload carries no PII beyond numeric values + recipe ref.

Regression test added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 10:42:16 +02:00
Adriano 742cc1fb58 fix(v2): two regressions surfaced by the local smoke test
Both came from the src/ restructure and only show up at runtime, so
the test suite had not caught them.

- src/backend/config.py: env_file was "../../.env", which pydantic-
  settings resolves against the *cwd*, not the file. Running uvicorn
  or alembic from the project root therefore looked for
  ../../.env one level above the repo and silently fell back to the
  default DB_PASSWORD ("change_me_in_production"), hiding the real
  password. Now resolved as Path(__file__).resolve().parents[2] /
  ".env" so the lookup is always against the project root regardless
  of cwd.

- src/backend/models/orm/__init__.py: Station and
  StationRecipeAssignment were never imported here, so anything that
  triggers Base.metadata.create_all without first importing the
  setup router (which has its own Station import) ended up with no
  stations / station_recipe_assignments tables. Verified locally:
  /api/setup/seed used to fail with "Table tiemeasureflow.stations
  doesn't exist" before this fix.

- .gitignore: ignore src/frontend/flask_app/package.json and
  package-lock.json (local npm-install artifacts; the Dockerfile
  installs tailwindcss directly).

Smoke verified end-to-end: uvicorn + gunicorn + MySQL, login + admin
stations + select_recipe + admin users all 200 OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:19:36 +02:00
Adriano e4b29c0b2d chore(docs): consolidate documentation, drop dead files
Cleanup
- Remove src/backend/Dockerfile.legacy and
  src/frontend/flask_app/Dockerfile.legacy (history is in git, build
  uses the new uv-based root Dockerfile / Dockerfile.frontend).
- Remove src/frontend/flask_app/verify_i18n.py (had hardcoded paths
  pointing at the old client/ tree).

Group docs/
- New docs/README.md indexes everything in one place.
- New docs/architecture/STATO_PROGETTO.md: snapshot of what works in
  V2.0.0 (inherited V1.0.7 features, rev04 Phase 1 stations,
  worker scaling, src/ restructure, test status, stack, decisions).
- New docs/architecture/ROADMAP.md: what's next — Phases 2-7 of the
  rev04 migration with status, open client decisions (D-0.1 through
  D-0.10), tech debt and time estimates for M1 / M2.
- Move PIANO_IMPLEMENTAZIONE.md (90KB V1.0.0 plan) to
  docs/archive/2026-02-06-piano-implementazione-v1.md (historical).
- Move Schema sviluppo SW TieFlow_rev04-2026.docx to docs/specs/
  with ISO date filename so the customer spec is now tracked.
- Move src/frontend/flask_app/I18N_SETUP.md to docs/I18N_SETUP.md
  and rewrite paths to the new src/frontend/flask_app/ tree.

.dockerignore: simplified now that legacy Dockerfiles are gone;
docs/ stays excluded from the build context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:43:57 +02:00