feat: FASE 7 - Polish & Testing (security, i18n, test suite, docs)

Security hardening: CORS lockdown, rate limiting middleware con sliding
window e eviction IP stale, security headers (CSP, HSTS, X-Frame-Options),
session cookie hardening, filename sanitization upload.

i18n completion: internazionalizzati barcode.js e csv-export.js con bridge
window.BARCODE_I18N/CSV_I18N, aggiornati .po IT/EN con 27 nuove stringhe.

Tablet UX: touch target 44px per dispositivi coarse pointer.

Test suite: 101 test totali (76 server + 25 client), copertura completa
di tutti i router API, autenticazione, ruoli, CRUD, SPC, file upload,
security integration. Infrastruttura SQLite async in-memory con fixtures.

Fix critici: MissingGreenlet in recipe_service (selectinload eager),
route ordering tasks.py, auth_service bcrypt diretto, Measurement.id
Integer per SQLite.

Documentazione: API.md (riferimento completo 40+ endpoint),
DEPLOYMENT.md (guida produzione con Docker/Nginx/SSL),
USER_GUIDE.md (manuale utente per ruolo).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Adriano
2026-02-07 17:10:24 +01:00
parent 26e5b9343d
commit dd2ebf863a
46 changed files with 6322 additions and 90 deletions
+26
View File
@@ -1,3 +1,29 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Tablet Touch Targets (44px minimum) */
@layer components {
.touch-target {
min-height: 44px;
min-width: 44px;
}
}
@media (pointer: coarse) {
button,
.btn,
[role="button"],
a.inline-flex {
min-height: 44px;
}
/* Flash message dismiss button */
.fixed button[x-data] {
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
}
+28
View File
@@ -372,3 +372,31 @@ textarea:focus-visible {
[x-cloak] {
display: none !important;
}
/* ============================================================
Tablet Touch Targets (44px minimum per WCAG 2.5.5)
============================================================ */
.touch-target {
min-height: 44px;
min-width: 44px;
}
@media (pointer: coarse) {
button,
.btn,
[role="button"],
a.inline-flex {
min-height: 44px;
}
/* Flash message dismiss button */
.fixed button[x-data] {
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
}
+5 -3
View File
@@ -5,6 +5,8 @@
*/
function barcodeScanner() {
const _t = (key) => (window.BARCODE_I18N && window.BARCODE_I18N[key]) || key;
return {
scanning: false,
result: null,
@@ -19,7 +21,7 @@ function barcodeScanner() {
// Check library availability
if (!window.Html5Qrcode) {
this.error = 'Scanner library not loaded';
this.error = _t('scanner_lib_not_loaded');
this.scanning = false;
return;
}
@@ -29,7 +31,7 @@ function barcodeScanner() {
const devices = await Html5Qrcode.getCameras();
if (!devices || devices.length === 0) {
this.error = 'Nessuna fotocamera disponibile';
this.error = _t('no_camera_available');
this.scanning = false;
return;
}
@@ -72,7 +74,7 @@ function barcodeScanner() {
} catch (err) {
console.error('Scanner initialization error:', err);
this.error = 'Impossibile accedere alla fotocamera';
this.error = _t('camera_access_error');
this.scanning = false;
}
},
+32 -30
View File
@@ -5,6 +5,8 @@
*/
function csvExport() {
const _t = (key) => (window.CSV_I18N && window.CSV_I18N[key]) || key;
return {
// Locale-specific settings
delimiter: ';', // Italian Excel standard
@@ -50,20 +52,20 @@ function csvExport() {
buildMeasurementCSV(measurements) {
const headers = [
'ID',
'Subtask ID',
'Nome Sottotask',
'Valore Misurato',
'Unità',
'Valore Nominale',
'Tolleranza +',
'Tolleranza -',
'Scarto',
'Esito',
'Numero Lotto',
'Numero Seriale',
'Metodo Input',
'Data Misurazione',
'Operatore'
_t('subtask_id'),
_t('subtask_name'),
_t('measured_value'),
_t('unit'),
_t('nominal_value'),
_t('tolerance_plus'),
_t('tolerance_minus'),
_t('deviation'),
_t('result'),
_t('lot_number'),
_t('serial_number'),
_t('input_method'),
_t('measurement_date'),
_t('operator')
];
let csv = headers.join(this.delimiter) + '\n';
@@ -97,28 +99,28 @@ function csvExport() {
*/
buildTaskSummaryCSV(task) {
// Header section
let csv = 'RIEPILOGO ESECUZIONE TASK\n\n';
let csv = _t('task_summary_title') + '\n\n';
csv += `Task ID${this.delimiter}${task.id}\n`;
csv += `Nome Task${this.delimiter}${this.escapeCsvValue(task.name || '')}\n`;
csv += `Ricetta${this.delimiter}${this.escapeCsvValue(task.recipe_name || '')}\n`;
csv += `Operatore${this.delimiter}${this.escapeCsvValue(task.operator_name || '')}\n`;
csv += `Data Inizio${this.delimiter}${this.formatDateTime(task.started_at)}\n`;
csv += `Data Fine${this.delimiter}${this.formatDateTime(task.completed_at)}\n`;
csv += `Stato${this.delimiter}${task.status || ''}\n`;
csv += `Lotto${this.delimiter}${this.escapeCsvValue(task.lot_number || '')}\n`;
csv += `Seriale${this.delimiter}${this.escapeCsvValue(task.serial_number || '')}\n`;
csv += `${_t('task_id')}${this.delimiter}${task.id}\n`;
csv += `${_t('task_name')}${this.delimiter}${this.escapeCsvValue(task.name || '')}\n`;
csv += `${_t('recipe')}${this.delimiter}${this.escapeCsvValue(task.recipe_name || '')}\n`;
csv += `${_t('operator')}${this.delimiter}${this.escapeCsvValue(task.operator_name || '')}\n`;
csv += `${_t('start_date')}${this.delimiter}${this.formatDateTime(task.started_at)}\n`;
csv += `${_t('end_date')}${this.delimiter}${this.formatDateTime(task.completed_at)}\n`;
csv += `${_t('status')}${this.delimiter}${task.status || ''}\n`;
csv += `${_t('lot')}${this.delimiter}${this.escapeCsvValue(task.lot_number || '')}\n`;
csv += `${_t('serial')}${this.delimiter}${this.escapeCsvValue(task.serial_number || '')}\n`;
// Statistics
csv += `\nSTATISTICHE\n`;
csv += `\n${_t('statistics')}\n`;
const stats = this.calculateStats(task.measurements);
csv += `Totale Misure${this.delimiter}${stats.total}\n`;
csv += `Passate${this.delimiter}${stats.passed}\n`;
csv += `Fallite${this.delimiter}${stats.failed}\n`;
csv += `Percentuale Successo${this.delimiter}${this.formatNumber(stats.passRate)}%\n`;
csv += `${_t('total_measurements')}${this.delimiter}${stats.total}\n`;
csv += `${_t('passed')}${this.delimiter}${stats.passed}\n`;
csv += `${_t('failed')}${this.delimiter}${stats.failed}\n`;
csv += `${_t('pass_rate')}${this.delimiter}${this.formatNumber(stats.passRate)}%\n`;
// Measurements detail
csv += `\nDETTAGLIO MISURE\n`;
csv += `\n${_t('measurement_details')}\n`;
csv += this.buildMeasurementCSV(task.measurements);
return csv;