Pannello visione overflow-hidden/min-h-0, immagine e overlay object-contain
sovrapposti nel riquadro flex-1, colonna esito con scorrimento proprio.
Test sulle classi del template.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rotte maker: elenco stazioni con camera, grab per stazione, stato calibrazione
dall'acquisitore (C1), inoltro a B3. Pagina stazioni: stato e link «Calibra».
Test con api_client e requests finti; traduzioni IT/EN.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Route POST /measure/api/vision/grab (proxy verso il servizio camera),
get_station_by_code, pulsante «Acquisisci dalla camera», grab() in
vision_execute.js, campi camera_url e camera_source nell'admin stazioni,
traduzioni it/en, extra_hosts host.docker.internal nel compose di sviluppo.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Dopo il copy-on-write del backend (modifica su versione con misure ->
nuovi id per task e quote), task_editor.html teneva in memoria gli id
vecchi: il salvataggio successivo riceveva 409 "Cannot modify a
non-current version". Ogni handler di salvataggio (create/update/delete
di task e quote, reorder, upload immagine) ora ricarica la pagina su
409, mostrando un avviso dopo il reload. updateTask/updateSubtask/
addSubtask ricaricano anche su successo se l'id restituito (recipe_id
task, task_id quota) non corrisponde a quello locale - il copy-on-write
avvenuto silenziosamente.
Stringa aggiunta ai due cataloghi .po. Test: proxy Flask che verifica
il passaggio del 409 (test_maker.py), sintassi JS dello script inline
di task_editor.html con node --check (test_template_js_syntax.py).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
recipe_editor.html saveRecipe(): i due campi vengono sempre inviati nel
payload (valore o null esplicito), non più omessi quando svuotati.
recipe_service.py _header_fields(): per i due campi (unica eccezione
concordata al confine dei lotti, righe ~37-60) distingue "non inviato" da
"inviato null" con model_fields_set, invece di getattr(...) is not None —
altrimenti un null esplicito veniva scartato allo stesso modo di un campo
assente e il valore restava bloccato per sempre. Gli altri campi header
(name, description, requires_*, allow_manual_input) non toccati.
Test backend in test_recipes.py: TestUpdateRecipeClearNullableFields.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1. api_client.py: ConnectionError/Timeout restituisce status_code=502 invece
di 0 (nessuna route confrontava == 0, verificato); i proxy che inoltrano
resp.get("status_code", 500) ereditano il fix senza altre modifiche.
4. measure.py lookup_barcode: code non stringa -> 400 (prima 500 su
code.strip()); urllib.parse.quote(code, safe="") prima di comporre l'URL
verso il backend - un barcode con '/' o '../' non spezza più il path né
raggiunge altri GET con i permessi dell'utente.
5. Ogni "'{{ _(...) }}'" dentro un attributo Alpine (x-text, :title,
:placeholder, @click) sostituito con "{{ _(...)|tojson_attr }}": la
traduzione EN di admin/stations.html:105 ("Clear the station's recipes")
ha un apostrofo che l'IT non ha, e rompeva quel binding e basta - non è
un XSS, ma stessa classe di difetto di #10. File toccati: admin/stations,
admin/users, maker/recipe_list, maker/recipe_editor, maker/task_editor,
maker/task_drawing, components/caliper_status, measure/select_recipe.
Le occorrenze dentro <script>...</script> (non attributi) sono fuori
perimetro per questo punto.
6. statistics.py dashboard(): ciclo sulle pagine (per_page=100, il massimo
del backend) invece di un'unica chiamata che troncava la tendina ricette
alla centesima.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
L'inattività contava mousemove/mousedown/keypress/touchstart/scroll. Il
numpad fa preventDefault() su ogni cifra/Enter letta dal calibro USB, il che
sopprime anche il keypress successivo: un operatore che lavora solo di
calibro non generava alcuna attività e veniva disconnesso a metà task.
Aggiunto 'keydown' alla lista: scatta comunque, a prescindere dal
preventDefault() di un handler successivo sullo stesso evento.
Test: rendering di una pagina con auto_logout_minutes impostato, verifica
che l'elenco eventi contenga 'keydown'.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
#13 — handleMeasurement era rientrante: saving tornava false prima della
pausa di 1s di feedback, currentIndex avanzava dopo. Una seconda lettura in
quella finestra salvava due volte la stessa quota e saltava la successiva.
Guardia unica this.busy: true all'ingresso, false su ogni uscita della
funzione; l'ingresso è bloccato anche con showCompletionOverlay,
showSupervisorModal, isPaused o cycleConfirmed pendenti. Una lettura scartata
non sparisce in silenzio: errorMessage "Lettura ignorata: attendere".
#12 — dopo la scadenza del token CSRF (config lato server, lotto B2) il POST
tornava 400 in HTML: response.json() lanciava, finiva nel catch generico e
l'operatore vedeva "Errore di rete" invece che "Sessione scaduta". Aggiunto
try/catch dedicato sul parsing e controllo di result.csrf_expired /
session_expired, sia in handleMeasurement che in postProduction(); la lettura
non salvata resta sul display.
#29 — confirmCycle mostrava il ciclo chiuso anche quando il server rifiutava
/cycle (produzione aperta, postProduction torna null): cadeva nel ramo "nessuna
produzione", incrementando cycleCount in locale. Ora, con produzione aperta, un
rifiuto del server ripristina cycleConfirmed=false, riapre l'overlay di
completamento e mostra l'errore (già in productionError); il ramo locale resta
riservato a quando non c'è produzione aperta.
#37 punto 9 — il blocco capoturno era deciso su passFailStatus (preview
client, prima del salvataggio) invece che su result.pass_fail del server;
ora la risposta del server decide sia il blocco che i conteggi locali.
Completa anche il lato pagina di #27: aggiunto il listener per l'evento
numpad-error (dispatchato da numpad.js) cosi' la lettura rifiutata dal calibro
è visibile all'operatore, non solo intercettata in silenzio.
Stringhe nuove in messages.po (it/en): "Sessione scaduta: ricaricare la
pagina", "Lettura ignorata: attendere", "Lettura del calibro rifiutata: valore
troppo grande".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
lotNumber/serialNumber erano '{{ lot_number or '' }}': autoescape HTML dentro
uno <script>, non escaping JS/JSON. '&' diventava '&' e finiva salvato nel
DB a ogni misura e a production/start; un lotto con backslash rompeva la
sintassi JS della pagina. |tojson produce un letterale JSON valido sia come JS
che come HTML, come già fa refuseWhileBlocked() più sotto nello stesso file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
config.py: guard_default_secret_key, verificata a ogni richiesta (prima di
create_app, TESTING/DEBUG non sono ancora impostati dai fixture di test) —
rifiuta con RuntimeError se CLIENT_SECRET_KEY è rimasto al default committato
fuori da debug/testing. auth.py: session.permanent=True al login (senza,
PERMANENT_SESSION_LIFETIME non aveva mai effetto); /auth/logout solo POST,
con CSRF. Aggiornati i tre punti che lo chiamavano: profile.html e
navbar.html (link -> form POST), auto-logout JS in base.html (fetch POST +
redirect al login).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
select_recipe.html e recipe_list.html interpolavano recipe/lot/serial/search
dentro un letterale JS con apici singoli: un valore con `'` chiudeva la
stringa e iniettava chiavi nell'oggetto x-data (CSP con unsafe-eval).
Anche recipe_list.html:222 (toLocaleDateString) aveva lo stesso schema su
current_language. Fix: |tojson_attr ovunque.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>