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>
Punto 14. La segnalazione dell'operatore del 28/07 non era stata circoscritta sul
codice: «le dimensioni delle viste cambiano a seconda del menu». Sono quattro
meccanismi distinti, tutti leggibili senza avere il tablet in mano.
Ogni vista si dichiarava la propria larghezza: sette valori diversi su diciassette
template, e nessuno coincideva con quello della navbar, che sta a max-w-7xl su
tutte. Il contenuto era quindi disallineato dalla barra sopra di sé, e il
disallineamento cambiava a ogni pagina. Il percorso dell'operatore faceva
1280 → 1024 → tutto schermo → 1280 in quattro passaggi.
Il padding verticale mescolava py-8 e py-6 fra viste consecutive, così la prima
card cambiava altezza a ogni schermata.
La barra di scorrimento è classica da 8px e occupa spazio: una pagina lunga la
mostrava, una corta no, e la schermata di misura la toglie sempre. A ogni
navigazione tutto il contenuto centrato — navbar compresa — si spostava di 8px.
La schermata di misura era alta 100vh, che su Android e iOS è l'altezza con la
barra dell'indirizzo nascosta: su un tablet il piede, dove stanno «Fine ciclo
misura» e il tastierino, finiva sotto il bordo.
Ora: .tmf-page e .tmf-page-narrow in themes.css, con i valori esatti della navbar;
scrollbar-gutter: stable; 100dvh dove serve. Due larghezze in tutto il prodotto al
posto di sette, e il criterio è il tipo di pagina — liste, tabelle e tele stanno
larghe, i moduli stanno stretti.
Login e schermata di misura tengono la loro geometria, per i motivi scritti in
docs/architecture/LAYOUT.md e ripetuti in EXEMPT dentro il test: la prima non ha
navbar a cui allinearsi, la seconda non deve scorrere mentre si misura.
Il test è statico e guarda i sorgenti: la vista scritta domani copierà la cornice
dalla vicina, ed è lì che la deriva ricomincia.
Resta da chiedere all'operatore su quale schermo l'ha vista: in verticale su un
tablet quasi tutte quelle larghezze collassano e il difetto non si nota.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>