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>
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
* Used for measurement data entry in task_execute.html
|
||||
*/
|
||||
|
||||
function numpad() {
|
||||
function numpad(options) {
|
||||
var opts = options || {};
|
||||
|
||||
return {
|
||||
// State
|
||||
value: '', // String representation of the current value
|
||||
@@ -13,9 +15,16 @@ function numpad() {
|
||||
maxIntDigits: 6, // Maximum integer digits
|
||||
maxDecDigits: 6, // Maximum decimal digits
|
||||
|
||||
/* Whether a value may be typed at all, as the recipe declares it. When false
|
||||
the keypad shows no digits and a value that looks typed is refused here as
|
||||
well as by the server: the caliper is the instrument. Defaults to allowed,
|
||||
so a caller that says nothing gets the behaviour that came before. */
|
||||
allowManual: opts.allowManual !== false,
|
||||
|
||||
// HID burst detection (USB caliper vs manual typing)
|
||||
_lastKeyTime: 0, // Timestamp of last keystroke
|
||||
_burstCount: 0, // Consecutive fast keystrokes
|
||||
_enterWasFast: false, // Enter arrived in the same burst as the digits
|
||||
|
||||
/**
|
||||
* Get the display value with sign
|
||||
@@ -109,6 +118,20 @@ function numpad() {
|
||||
this.value = '';
|
||||
this.negative = false;
|
||||
this.hasDecimal = false;
|
||||
this._enterWasFast = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* How the value in the display got there.
|
||||
*
|
||||
* A wedge caliper sends its digits and the Enter that follows them as one
|
||||
* burst; a person is slower on both. The Enter is the stronger of the two
|
||||
* signals: a short reading like "9.5" is only three keystrokes, too few to
|
||||
* judge by count alone, and used to be filed as typed by hand.
|
||||
*/
|
||||
_classifyInput() {
|
||||
if (this._enterWasFast) return 'usb_caliper';
|
||||
return this._burstCount >= 3 ? 'usb_caliper' : 'manual';
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -118,9 +141,18 @@ function numpad() {
|
||||
if (!this.hasValue) return;
|
||||
|
||||
const val = this.numericValue;
|
||||
const inputMethod = this._classifyInput();
|
||||
|
||||
// Determine input method: 3+ fast keystrokes = USB caliper burst
|
||||
const inputMethod = this._burstCount >= 3 ? 'usb_caliper' : 'manual';
|
||||
// The recipe forbids typing: say so and keep the value on screen rather than
|
||||
// clearing it, so the operator sees what was refused. The server refuses the
|
||||
// same request anyway - this is only the earlier, kinder of the two answers.
|
||||
if (inputMethod === 'manual' && !this.allowManual) {
|
||||
this.$dispatch('numpad-rejected', { reason: 'manual_not_allowed', value: val });
|
||||
this._enterWasFast = false;
|
||||
this._burstCount = 0;
|
||||
this._lastKeyTime = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Dispatch custom event for parent component to handle
|
||||
this.$dispatch('numpad-confirm', { value: val, inputMethod: inputMethod });
|
||||
@@ -199,6 +231,12 @@ function numpad() {
|
||||
// Enter - confirm
|
||||
else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
// Measured before confirming: an Enter that lands within the burst is the
|
||||
// caliper closing its own transmission, not a person reaching for a key.
|
||||
const gap = this._lastKeyTime > 0
|
||||
? performance.now() - this._lastKeyTime
|
||||
: Infinity;
|
||||
this._enterWasFast = gap < 80;
|
||||
this.confirm();
|
||||
}
|
||||
// Minus sign - toggle sign
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Task descriptions: line breaks and bold, and nothing else.
|
||||
*
|
||||
* Whoever writes a recipe pastes from the PDF of the technical sheet, and the text
|
||||
* used to arrive flattened - every line run into the next, every emphasis lost, to
|
||||
* be put back by hand each time. Two conventions carry it now: a newline is a line
|
||||
* break, **like this** is bold.
|
||||
*
|
||||
* No HTML is stored or trusted. The text is escaped first and the only tags in the
|
||||
* result are the ones produced here, so a description that contains <script> shows
|
||||
* those characters and does nothing. The server-side filter (rich_text in app.py)
|
||||
* follows exactly these rules, so a description reads the same whether the page
|
||||
* was rendered by Flask or by Alpine.
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/* Escaped text, then the two tags we add ourselves. Order matters: escaping
|
||||
after the substitution would turn our own <strong> into visible text. */
|
||||
function richText(value) {
|
||||
if (!value) return '';
|
||||
return escapeHtml(value)
|
||||
.replace(/\*\*([\s\S]+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
/* Wrap whatever is selected in a textarea in ** **, the way a B button does.
|
||||
Dispatching 'input' is what keeps x-model in step: assigning to value alone
|
||||
changes the DOM and leaves Alpine with the old string. */
|
||||
function wrapSelectionBold(el) {
|
||||
if (!el) return;
|
||||
var start = el.selectionStart;
|
||||
var end = el.selectionEnd;
|
||||
var text = el.value || '';
|
||||
el.value = text.slice(0, start) + '**' + text.slice(start, end) + '**' + text.slice(end);
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
el.focus();
|
||||
// Leave the cursor around the same words, now inside the markers.
|
||||
el.selectionStart = start + 2;
|
||||
el.selectionEnd = end + 2;
|
||||
}
|
||||
|
||||
global.richText = richText;
|
||||
global.wrapSelectionBold = wrapSelectionBold;
|
||||
})(window);
|
||||
Reference in New Issue
Block a user