GET /measure/api/task-progress/<task_id>: il browser non puo' chiamare
/api/measurements/task-progress del backend (niente X-API-Key). Login +
stesso controllo di stazione di task_execute() (_recipe_allowed_at_station),
poi inoltra a _task_progress() con lo stesso version_id/production_run_id
gia' usato da task_list.html.
Non tocca task_execute.html: /api/measurements/task-progress risponde
con conteggi aggregati per task (quotes/measured/state), non con le
quote-per-quota misurate (subtask_id, value, pass_fail, deviation) che
`this.measurements` richiede - ripristinarlo avrebbe richiesto inventare
valori o uno stato non offerto dal backend. MANCA: mostrato in dettaglio
nel rapporto finale.
Test Flask della route: riga per il task richiesto, default a "empty"
se assente, 403 fuori stazione, propagazione 404, login richiesto.
Co-Authored-By: Claude Sonnet 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>
Aggiunte a mano in coda a it/en messages.po (nessuna rigenerazione):
"Sessione scaduta: effettua nuovamente l'accesso." (#12/#25),
"Ricetta non assegnata a questa stazione" (#24), "Codice non valido" (#37.4).
test_admin_stations_inline_js_is_valid_in_english (#37.5): i .mo sono
gitignored e compilati solo in Docker, non in questo run - impostare
session["language"]="en" da solo non aveva alcun effetto e il test passava
senza esercitare il bug EN reale. Ora patcha jinja_env.globals["_"] con la
traduzione nota per quella stringa, verificato che fallisca davvero sul
codice pre-fix (ripristinato subito dopo) prima di lasciarlo verde.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_fitCanvasToImage() prima scalava solo canvas e sfondo: gli oggetti già
piazzati restavano alle vecchie coordinate pixel, e un salvataggio subito
dopo scriveva il nuovo imageScale accanto a posizioni mai spostate -
markers/frecce/aree finivano fuori posto anche nel viewer dell'operatore.
Ora, quando la scala cambia (f = newScale/oldScale, conservata in
this._currentScale), ogni oggetto viene ricalcolato coerentemente con
getAnnotationsJson/loadAnnotationsJson (il riferimento):
- left/top sempre *= f
- area (fabric.Rect): width/height *= f, non scaleX/scaleY - è quello che
getAnnotationsJson legge e loadAnnotationsJson ricostruisce
- text (IText): fontSize *= f, stesso motivo
- marker/arrow (Group): scaleX/scaleY *= f, il loro width/height non viene
mai riletto al caricamento
- arrow: arrowX1/Y1/X2/Y2 *= f, sono nello stesso spazio di left/top
(li aggiorna anche object:modified con un delta) e finiscono nel JSON
Aggiunta guardia beforeunload quando isDirty (annotazioni non salvate).
Test in tests/js/test_annotation_editor_resize.js (node, senza Fabric.js
reale: richiede jsdom non installato e il progetto non ha npm deps -
canvas stand-in minimale che copre solo la superficie usata da
_fitCanvasToImage). annotation-viewer.js non toccato.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
config.py: default invariato (sicuro salvo FLASK_DEBUG=1); nuova env
SESSION_COOKIE_SECURE=false la spegne esplicitamente, per l'installazione
on-premise dove nginx serve HTTP in LAN - un cookie Secure su origine non
HTTPS viene scartato dal browser in silenzio e il login non regge mai.
.env.example documenta la variabile (commentata, comportamento invariato
di default). docker-compose e default dev restano al lotto C2.
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>
Stessa regola del backend (neutralise_csv_cell): un valore che, tolti gli
spazi iniziali, comincia con =, +, -, @, TAB o CR riceve un apice davanti
prima di finire in cella - Excel/LibreOffice lo trattano come testo invece
che come formula. Solo per campi testuali (lot/seriale/descrizione):
formatNumber, usato per le colonne numeriche, non passa da qui, quindi un
valore negativo resta un numero. Aggiunto anche il quoting per \r, che
prima passava non quotato.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
buildMeasurementCSV leggeva m.subtask_name/unit/nominal/tolerance_plus/
tolerance_minus/operator_name: nessuno di questi campi esiste su
MeasurementResponse. I dati reali stanno annidati in m.subtask (description,
unit, nominal, utl, ltl - misurati.py:task_complete() li allega lato server).
tolleranza + = utl-nominal, tolleranza - = nominal-ltl, solo quando entrambi
i lati sono presenti (nessun NaN su tolleranza a un lato solo). Unità dalla
quota, "mm" solo se assente. Operatore: measured_by (id), l'unico campo
reale - non esiste un nome operatore da nessuna parte in questi dati.
Test node in tests/js/test_csv_export.js (stesso pattern di test_numpad.js).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
task_complete(): GET /api/measurements ora porta lot_number/serial_number
(sessione) + measured_by (utente corrente), invece del solo version_id -
una seconda produzione sulla stessa versione non pesca più le misure di
altre stazioni/giorni. Risposte oltre 500 misure vengono cicliche per
pagina invece di troncate al primo per_page=500.
production_run_id sarebbe il filtro più preciso ma GET /api/measurements
non lo accetta (lotto A3) e task_execute.html non lo passa in query
(lotto B1): MANCA, annotato nel codice.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_recipe_allowed_at_station() riusa get_station_recipes() (stessa fonte di
/measure/select) per lookup_barcode, start_sequence e task_execute: prima
solo l'elenco filtrava, il barcode e i link diretti lasciavano misurare
ricette non assegnate alla stazione. Ricetta non assegnata -> abort(403)
con messaggio "ricetta non assegnata a questa stazione" (pagine, come già
fa role_required) o JSON 403 (lookup-barcode). Aggiornati i test esistenti
che ora richiedono get_station_recipes/STATION_CODE mockati.
Backend (ruoli sui run, StationRecipeAssignment su open_run) resta al
lotto A1.
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>
toggleSign() usciva subito a display vuoto: il segno del calibro, che arriva
prima delle cifre, veniva scartato. Ora '-'/'+' impostano il segno in modo
esplicito (non toggle) quando arrivano da tastiera/HID; toggleSign() (bottone
manuale +/-) funziona anche a vuoto.
_trackBurst riconosce l'inizio di una raffica solo dal secondo carattere:
prima ripuliva il display in ritardo, concatenando cifre digitate a mano.
Ora la raffica ripulisce il display fino al proprio inizio quando la pausa
prima di essa è > 300ms. Parte intera oltre 6 cifre: rifiutata con evento
numpad-error invece di essere troncata in silenzio.
Test node in tests/js/test_numpad.js, lanciato da pytest
(tests/test_numpad_js.py, skip se node manca).
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>
api_client._handle_response: su 401 pulisce la sessione Flask e marca
g.session_expired. Un solo after_request (app.py) trasforma le pagine HTML
rimaste in piedi con un errore generico in un redirect al login con flash;
le route JSON/AJAX che già inoltrano il dict di api_client (measure.py,
statistics.py) ricevono session_expired nel corpo insieme al 401.
Backend (riuso della key, bcrypt a tempo costante) resta al lotto A2.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
WTF_CSRF_TIME_LIMIT=None: la pagina di misura resta aperta ore, il token a
1h scadeva a metà turno. Aggiunto errorhandler(CSRFError) in app.py: risposte
JSON/fetch (Accept, Content-Type, X-Requested-With) ricevono 400 JSON con
csrf_expired, le altre un flash + redirect. Lato B1 (task_execute.html) resta
da gestire client-side, fuori da questo commit.
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>
Il controllo sui riferimenti esterni scandiva anche i file che git non
conosce. Una copia di comodo di una libreria lasciata in static/js/ --
fabric-debug.js -- sembrava codice nostro e faceva fallire la prova con gli
URL nei propri commenti, ma non sta nel repository e in reparto non ci
arriva mai. Su un checkout pulito la prova passava: falliva solo su chi
aveva quel file, cioe' proprio su chi stava lavorando.
Il filtro ora e' la tracciabilita', non il percorso. Non i pattern di
.gitignore, che non dicono nulla su un file gia' tracciato. Se git non
risponde si scandisce tutto come prima: una prova-guardia che ammutolisce
quando perde l'appoggio e' peggio di una che grida al lupo.
Verificato che morda ancora: un fetch verso un CDN in un file tracciato la
fa fallire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
Punto 12. Le cinque librerie erano già state portate in casa (28ee44b): nessun
template carica più niente dalla rete, il worker di PDF.js è locale in tutti e
quattro i file che lo impostano, i font sono woff2 nel pacchetto. Verificato riga
per riga, e le impronte SHA-256 in VERSIONS.md corrispondono ancora.
Mancava però la seconda metà dell'intervento, e mancava dove conta. La
Content-Security-Policy a sola origine locale esisteva sul backend, cioè sulle
risposte API; le pagine HTML le serve il client Flask, che non mandava alcuna
policy. La regola stava scritta dove non poteva essere infranta e assente dove
poteva. Ora il client la manda su ogni risposta.
Serve meno a difendere e più a non far tornare indietro il punto: un tag verso un
CDN aggiunto fra sei mesi viene rifiutato dal browser alla scrivania, dove c'è la
rete e l'errore si legge in console, invece che in reparto dove la rete non c'è.
Tailwind era ancora agganciato a `tailwindcss@3` nel Dockerfile: stesso difetto
che il documento cita per le librerie del browser, un gradino più in basso. Fissato
a 3.4.19, che è la versione con cui l'immagine in esercizio è stata costruita.
I test non renderizzano niente: leggono i sorgenti, perché il difetto che devono
impedire si scrive in un template e non si vede finché non si stacca la rete.
Controllano anche le impronte — una libreria sostituita sul posto tiene lo stesso
nome e la stessa riga in tabella, e l'hash è l'unica parte che se ne accorge.
Annotato in VERSIONS.md che `html5-qrcode` è l'unica libreria dichiarata e mai
caricata: lo scanner da fotocamera non è incluso da nessuna pagina, il lettore che
l'operatore usa è un campo di testo. Va portata in casa prima di accenderlo.
README aggiornato a V3.0.0: novità della versione punto per punto, ruolo
Supervisor, librerie locali al posto dei CDN, stato dei test.
Co-Authored-By: Claude Opus 5 (1M context) <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>
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>
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>
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>
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>
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>
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>
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>
- new mark: measurement-span glyph on rounded brand-gradient badge
(favicon, inline logo, standalone tmflow-logo.svg)
- wordmark moved from SVG <text> with hardcoded x offsets (clipped
"Flow" past the 260px viewBox, font-dependent overlap) to plain HTML
spans — never clips, natural kerning, theme-adaptive
- _app_logo.html params: logo_class, wordmark_class, logo_id (unique
gradient id when logo appears twice per page)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- login: theme variables instead of hardcoded slate colors, decorative
brand background, show/hide password toggle, dynamic copyright year
- navbar: nav links rendered via shared nav_link/nav_links macros
(desktop + mobile from one source), active state now also in mobile
menu, aria-current on active link, fix nonexistent w-4.5 class
- themes.css: btn :active press feedback, btn-lg variant, card hover
border accent, brand accent-color and ::selection
- fix tmf-input vs pl-10 cascade conflict (themes.css loads after
tailwind.css) with important utilities — also fixes recipe search icon
- app.py: inject current_year in template globals
- i18n: add Mostra/Nascondi password (IT/EN)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Remove the "1/2"/"2/2" prefix before the task title; the number circle
already shows the index.
- The directive showed only "…": line-clamp-1 + whitespace-pre-wrap clamped a
leading blank line to just the ellipsis. Use line-clamp-2 without pre-wrap so
the description is actually visible.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the per-task measurement count and attachment indicator out of the info
column to the top-right of each card (self-start), keeping the title/directive
column clean. Measurement count shown as a compact number+icon with tooltip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Merge the bottom "N task · M misurazioni" indicator into the top header
badge; remove the now-empty bottom navigation row.
- Make task cards shorter: smaller padding, smaller number circle, single-line
title (with N/M inline) and one-line directive, tighter spacing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The change-recipe button was only at the bottom of the task list, hard to
find. Move it just under the breadcrumb so operators can switch recipe
easily; the bottom row now keeps just the task/measurement count.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On the task list, each task card exposed a "Visualizza Task" link that jumps
straight into an individual task. Pure operators (MeasurementTec without
Maker) should follow the guided flow and start only with AVVIA. Gate the
per-task link behind the Maker role; admins/makers who also measure keep it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous change made the overlay phase-aware but it still popped up after
every task during the START run. Operators want to flow straight to the next
task. Skip showing the "Misurazioni Complete" overlay when production has not
started and this is not the last task — call goToNextTask() directly. The
overlay still appears on the last START task (for "Avvio Produzione") and in
production (for "Conferma ciclo").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When all subtasks of a task were measured, the completion overlay only
offered "Riepilogo" (which leaves the work cycle) or "Conferma ciclo" (which
keeps the operator stuck on the same task and starts the interval timer).
During the START run — before production is started — neither lets the
operator simply move to the next task.
Make the overlay phase-aware:
- Production started: keep "Conferma ciclo".
- START, not the last task: primary action "Task successivo" -> goToNextTask().
- START, last task: "Avvio Produzione" -> startProductionFromOverlay(), which
starts production and kicks off the interval cycle.
Add isLastTask getter and startProductionFromOverlay(); EN translation for the
new label.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two issues with the "Autorizzazione capoturno" modal:
1. Backspace and digits did not work in the username/password fields. The
numpad component installs a window-level keydown handler (for the USB
caliper / keyboard wedge) that preventDefault()s digits, Backspace, Enter
etc. It swallowed those keys inside the modal inputs. Guard handleKeydown
to ignore events whose target is an editable field (INPUT/TEXTAREA/SELECT/
contentEditable); the numpad has no text inputs of its own.
2. The modal kept the previous username/password: opening only set the flag,
and closing via backdrop click did not clear the fields. Add
openSupervisorModal()/closeSupervisorModal() that always reset username,
password and error, and use them for every open (fermo_linea,
fine_produzione, out_of_tolerance) and close (backdrop, Annulla).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
On the recipe selection page the Lotto/Seriale fields shared a single
filter bar with the "Cerca ricetta" search box, so operators read them as
search filters instead of data-entry fields and never filled them in.
Split into two cards: a search filter, and a distinct "Tracciabilità"
section with heading, helper text, accent border and monospace inputs with
amber/indigo icons matching the lot/serial badges shown downstream. Add EN
translations for the new strings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>