# Changelog

Tutte le modifiche rilevanti al progetto C4rgo Cloud sono documentate in questo file.

Il formato segue [Keep a Changelog](https://keepachangelog.com/it-IT/1.1.0/).
Il progetto usa [Semantic Versioning](https://semver.org/lang/it/).

## [5.22.0] - 2026-08-03

Revisione della sezione scanner in `/location`: tab ripuliti, comandi letti dal
device, storico GPS filtrabile.

### Aggiunto
- **Tab Status, Ethernet, WIFI, Mobile e SMS ricostruiti sui daemon del device.** Nessuno legge più da `RemoteInfo`, il censimento fermo al momento della registrazione. *Ethernet*: le interfacce arrivano da `list_interfaces` invece del nome cablato nel template (`IF_enx00e04c360706_IPV4_IP` — funzionava solo sullo scanner con quell'adattatore USB), con DHCP e IP statico. *WIFI*: stato, scansione con elenco cliccabile, connessione anche a reti nascoste; la password non resta a schermo dopo l'invio. *Mobile*: modem, SIM, operatore, segnale, stato dati e APN, con connessione/disconnessione e riavvio modem sotto conferma — se il device è in rete solo via mobile, quel comando fa cadere anche il canale che lo trasporta. *SMS*: **prima integrazione in assoluto** — i tipi `sms_send`/`sms_list`/`sms_read`/`sms_delete` esistevano da tempo su `c4rgo-modem` ma non erano mai stati collegati al cloud; elenco filtrabile, invio con contatore a 160 caratteri e conferma esplicita, perché un SMS si paga e non si annulla.
- **Tab "Status"**, che era un `div` vuoto popolato dal JS legacy. A differenza degli altri **non interroga il device**: legge lo snapshot persistito dal worker, così risponde subito e resta leggibile a scanner spento — che è quando lo si guarda. In cima una riga con stato raggiungibile/non raggiungibile e data dell'ultimo aggiornamento, in forma relativa e assoluta; oltre i 15 minuti di età lo snapshot viene marcato in rosso come *dato non aggiornato*. Senza quell'indicatore un dato fermo si legge come stato attuale: è esattamente il caso in cui il bridge era giù dal 27 luglio e il DB continuava a dichiarare *online dal 23*.
- **Tab "GPS" con storico sulla mappa e filtro per data funzionante.** I due datepicker nell'header della sezione *Location* non erano rotti: non erano **mai stati collegati** — i gestori `dp.change` calcolavano la data e poi facevano solo `blur()`, con la chiamata commentata. Ora vivono nel tab GPS e filtrano lo storico via `api_machine_gps_history`. La mappa disegna una **polilinea** sui punti diradati con una gerarchia visiva leggibile: pallino verde grande per la posizione corrente, arancione per l'inizio dell'intervallo, piccoli azzurri per i passaggi intermedi, ciascuno con popup di orario, coordinate, velocità e altitudine. La **soglia di diradamento è esposta** (10 m → 1 km, default 25 m) perché la distanza giusta dipende dall'uso: 25 m per seguire un mezzo, 500 m per vedere solo gli spostamenti fra siti. Lo stato riporta sempre quanti punti sono stati scartati (*"643 punti su 12.036"*): una mappa pulita che nasconde in silenzio il 95% dello storico sembra una mappa senza dati. Usa `C4Maps` e non `google.maps`, così il tab non è legato a un provider (il layer supporta anche Leaflet). La mappa è creata alla **prima apertura del tab** e con `triggerResize`: creata in un contenitore nascosto resterebbe un rettangolo grigio.
- **Tab "Comandi"** nella sezione scanner: l'elenco è letto **dal device** con `list_commands` di `c4rgo-cmd` (via l'endpoint generico `device-config`, già con controllo proprietario e whitelist servizi) ed eseguito con `execute_command_by_name` in modalità sincrona, mostrando exit code, stdout e stderr. Prima i pulsanti venivano da `macchina.Comandi`, cioè dalla tabella cloud `comandi`: mostravano ciò che il cloud *credeva* installato sullo scanner. L'esecuzione è **per nome e non per id** perché l'id nella risposta è quello della tabella `comandi` locale del device, che non coincide con quello del cloud: eseguire per id lancerebbe lo script sbagliato appena i due DB divergono. I comandi con `stato ≠ 1` restano visibili ma disabilitati, così si capisce che esistono e sono spenti invece di sembrare persi.
- **Endpoint storico GPS** `GET /api/v1/machine/{id}/gps-history` (`from`, `to`, `min_distance`, `max_points`), pensato per la mappa del tab GPS. I punti vengono **diradati per distanza** prima di essere serializzati: si tiene un punto solo se dista almeno 25 m dall'ultimo tenuto. Il worker scarta già i movimenti sotto i 10 m in scrittura, ma su finestre lunghe restano migliaia di punti sovrapposti — uno scanner fermo che pubblica ogni 30 s produce ~2.880 righe al giorno entro pochi metri. Misurato sui dati reali: da 69.723 a 10.963 punti (−84,3%), da 12.036 a 643 (−94,7%), da 8.341 a 352 (−95,8%). Oltre il tetto di 2.000 punti scatta un campionamento uniforme che **preserva primo e ultimo**; l'ultimo punto è comunque sempre conservato, perché è la posizione corrente. Una data senza orario (`2026-08-03`) copre l'intera giornata: senza questo, un filtro "dal 1 al 3" escluderebbe tutto il 3, e i dati sembrerebbero semplicemente mancanti.

### Modificato
- **Le label dell'interfaccia non nominano più il trasporto.** *Network state (MQTT)* → *Stato rete*, *No MQTT device associated with this machine* → *Dispositivo non associato*, *MQTT id* / *MQTT Device ID* → *ID dispositivo*, e i target di `form.MqttDeviceId` e `sa.MQTTDeviceID`. Anche i messaggi d'errore del tab Comandi sono generici: *dispositivo non raggiungibile* (504), *dispositivo non associato* (409). Restano invariati selettori CSS, nomi file e commenti nel codice — non sono mai mostrati — e le chiavi `resname`/`source` delle traduzioni, che cambiarle significherebbe rinominarle in tutte e 19 le lingue senza alcun effetto visibile.
- Aggiunte 6 trans-unit italiane e 5 inglesi per le stringhe nuove, fra cui `No device configured for this machine`, che era priva di traduzione e usciva in inglese anche in italiano.

### Rimosso
- **Il modulo SIM7600 legacy e il microservizio `c4rgoservice`**, resi irraggiungibili dai tab nuovi. Via `SIM7600ApiController` (21 rotte che rispondevano **tutte 500**), `SIM7600Service` (già orfano), `Sim7600MqttListenerCommand`, i tre template `_mobile_module`/`_gps_module`/`_sms_module`, ~87 KB fra `sim7600-modules.js`, `sim7600-sms-module.js` e il CSS, e `C4rgoServiceClient` (338 righe). Con essi i parametri `mqtt_*`/`c4rgo_service.*` in `services.yaml` e le env `MQTT_*` in `.env`, tutti senza consumatori. La causa del *"Device Offline"* da cui è partita la revisione era qui: **`C4RGO_SERVICE_URL` non era definita in alcun `.env`**, nemmeno in `.env.staging` — quel client non è mai stato istanziabile. Due indizi di quanto fosse morto quel codice: `templates/components/sim7600/` non esiste più, quindi l'include di `_sms_module` era rotto, e `versioned_asset('js/sim7600-sms-module.js')` puntava a `public/js/`, dove il file non c'è. **Conservati** `Sim7600DeviceStatus` e il suo repository: hanno nome legacy ma sono infrastruttura attiva del canale nuovo, li usano `ScannerMqttListenerCommand` e `MachineAdminController`.
- **I tab generati da `RemoteInfo.status.wizsetup`** (`reg`, `Data to ERP`, un secondo `Ethernet`, un secondo `Wifi`, `Bluetooth`, `Mobile/GPS`, `Plane`, `Crop`): venivano dal vecchio censimento device, duplicavano i tab nativi e non erano più aggiornati da nulla. Il dato `RemoteInfo` **resta** in DB e sull'entità, e continua ad alimentare i campi OS e IP nei tab nativi: è solo la resa a essere stata tolta.
- **L'elenco statico "Available Command"** sotto l'immagine dello scanner e il gestore `.class-cmd` rimasto orfano, che accodava su `comandi_exec` per il polling successivo del device senza alcun riscontro né output. La rotta `api_set_mac_cmd` resta per altri consumatori.

## [5.21.1] - 2026-08-03

### Sicurezza
- **Rimossa la password SMTP in chiaro** dal default `env(MAILER_URL)` in `config/services.yaml`, committata nel repo. È stata tolta l'intera riga, non solo la credenziale: `MAILER_URL` è un residuo dell'era SwiftMailer e nessuno la leggeva più (il mailer usa `MAILER_DSN`, `config/packages/mailer.yaml`); tutte le occorrenze nei `.env` erano già commentate. ⚠️ La password resta nella **storia git**: va **ruotata** sull'account interessato, la rimozione dal file non la rende innocua.

### Corretto
- **Con `APP_ENV` esportato nell'ambiente, nessun file `.env` veniva caricato.** `bin/console` e `public/index.php` condizionavano il caricamento a `if (!isset($_SERVER['APP_ENV']))` — residuo dello skeleton Symfony 4. Qualsiasi deploy che esporti `APP_ENV` (unit systemd con `Environment=APP_ENV=prod`, Docker, `SetEnv` di Apache, `fastcgi_param` di nginx) saltava in blocco `.env` **e** `.env.local`, quindi anche i segreti, fallendo alla prima variabile richiesta con `Environment variable not found`. Riguardava sia la CLI sia l'entry point web. Ora si usa `bootEnv()` non condizionato (forma corretta da Symfony 5.1): carica `.env.local.php` se presente, altrimenti l'intera catena `.env*`, **senza** sovrascrivere le variabili già nell'ambiente — che in produzione mantengono la precedenza. `config/bootstrap.php` (solo test) non era affetto.
- **`install-broker-baremetal.sh` non era idempotente su `LOCAL_PORT`**: pur preservando certificato e password, riportava la porta del listener locale al default 1883 a ogni riesecuzione. Su una macchina dove la 1883 appartiene al broker legacy (scenario previsto da `TEST-LAB.md` §0, che prescrive `LOCAL_PORT=1884`), mosquitto moriva con `Address already in use` — ma solo al riavvio successivo, ormai scollegato dal comando che aveva introdotto il guasto. Ora la porta già installata viene conservata leggendola da `conf.d/c4rgo-scanner.conf`; `LOCAL_PORT` esplicito mantiene la precedenza e la prima installazione resta 1883.

## [5.21.0] - 2026-08-02

### Aggiunto
- **Anteprima del desktop dentro la tabella scanner** (`/sa/scanners`): la riga si espande in una child row DataTables alta 480 px che monta il viewer **in sola lettura**, con un pulsante "Apri interattivo" che porta alla finestra a pagina intera (dove il superadmin ha mouse e tastiera). Il token viene coniato all'espansione — vive 90 s ed è monouso, quindi conierlo al caricamento della pagina lo farebbe scadere prima del click.
- **Una sola anteprima alla volta**: aprirne un'altra chiude la precedente, e passare alla finestra interattiva chiude l'anteprima. Ogni anteprima è una sessione WebRTC vera verso il Pi, non una miniatura: lasciarne aperte molte terrebbe occupati altrettanti encoder sugli scanner.
- **`App\Services\C4rgoDscRelayClient`**: il conio delle web-session, prima duplicato in `RemoteDesktopController`, è ora un servizio condiviso con l'anteprima inline.

### Corretto
- **"Apri interattivo" apriva la sessione nella stessa scheda** invece che in una nuova.
- **Il pulsante "← Chiudi" della pagina desktop non faceva nulla**: era un `window.close()` secco, che il browser consente solo a una scheda aperta da script. Ora la scheda viene aperta con `window.open()` (mantenendo `window.opener`, pagina same-origin) e il pulsante ripiega su `history.back()` o sulla pagina di provenienza quando la sessione è stata raggiunta direttamente.
- **I pulsanti 1:1 / Fit / Full del viewer** mostravano sempre "Fit" come selezionato: correzione nel bundle `c4rgo-dsc-web`, incluso qui in `public/vendor/c4rgo/`.

### Note di deploy
- Nessuna modifica di schema. Serve `cache:clear` (nuova rotta `sa_scanners_session`).

## [5.20.0] - 2026-08-01

### Aggiunto
- **Controllo remoto (mouse e tastiera) dal desktop web**, riservato al **superadmin**: `RemoteDesktopController` chiede `"input": true` al conio della web-session solo se l'utente ha `ROLE_SUPERADMIN`, e passa l'esito al viewer con `data-input`. Per tutti gli altri utenti la sessione resta in sola lettura, esattamente come prima. Il permesso non è deciso dal browser: viaggia con il token monouso ed è applicato dall'agent sul Pi, che per una sessione read-only non apre nemmeno il device uinput.

### Note di deploy
- Richiede relay e agent aggiornati (migrazione `0003_web_session_input.sql` sul relay) e il bundle viewer ricompilato, già incluso in `public/vendor/c4rgo/`.
- Se il relay non è aggiornato, il campo `input` viene ignorato e tutto resta in sola lettura: nessuna rottura.

## [5.19.0] - 2026-08-01

Gestione della flotta scanner con desktop remoto c4rgo-dsc: elenco superadmin
e associazione macchina ↔ device dall'anagrafica.

### Aggiunto
- **Pagina "Scanner remoti"** (`/{_locale}/sa/scanners`, `ROLE_SUPERADMIN`): elenco della flotta con desktop remoto, paginato/ordinato/ricercato **server-side** (DataTables, stessa convenzione di `SA/MacchineController`), filtrabile per **azienda** e **luogo** — la tendina dei luoghi si restringe all'azienda selezionata. Per ogni riga l'icona apre `machine_remote` in una nuova scheda; una checkbox include anche le macchine **senza** device associato, così si vede a colpo d'occhio cosa resta da configurare. L'ordinamento richiesto dal client passa da una whitelist di colonne prima di entrare nel DQL (`src/Controller/SA/ScannerRemoteController.php`, `templates/sa/scanners/list.html.twig`).
- Riquadro **"Scanner remoti"** nella dashboard superadmin, con il conteggio delle macchine che hanno `dsc_device_id` valorizzato.
- **Campo `dscDeviceId` nell'anagrafica macchina** (`MacchineType`, sezione *Hardware Identity*): validato come UUID, opzionale (vuoto = desktop remoto disabilitato), con nota su dove leggerlo (output di `c4rgo-dsc-agent enroll` o `/var/lib/c4rgo-dsc-agent/state.json`). Chiude l'associazione scanner ↔ device, che finora richiedeva un `UPDATE` SQL a mano.
- Il device id compare nella **scheda di dettaglio macchina** con la scorciatoia di apertura sessione, e le etichette sono tradotte (`form.DscDeviceId`, it/en).

### Note di deploy
- Nessuna modifica di schema: `macchine.dsc_device_id` è già portata da `docker/mqtt/ensure-mqtt-schema.sql` (5.18.0). Serve solo `cache:clear` (rotte + traduzioni).
- I thumbnail del desktop nella pagina elenco sono volutamente esclusi: richiederebbero uno snapshot lato agent e un servizio che parli Noise (il backend PHP non può). Analisi in `rust/c4rgo-dsc/docs/WEB-INTEGRATION.md` §8.

## [5.18.1] - 2026-07-27

### Aggiunto
- **Pannello "Azioni dispositivo"** nel tab Amministrazione: push di configurazione al device via MQTT con i `type`/payload **verificati** contro i daemon `c4rgo-network` e `c4rgo-modem` (workspace `rust/c4rgo-workspace`, envelope `{type, payload}` snake_case). Comandi cablati: Wi-Fi connect (`network/wifi_connect` — ssid/password/interface), IP `set_dhcp` / `set_static_ip` (interface + address CIDR/gateway/dns), `force_connectivity_check`, `restart_network_manager` (con conferma), modem `data_connect` / `data_disconnect` / `modem_restart` (con conferma). Feedback inline + gestione del 504 (device offline).

### Corretto
- La nota di 5.18.0 sui `type` MQTT "da confermare": ora sono confermati e mappati in `public/static/js/mqtt-admin.js` (funzione `buildPayload`), unico punto da estendere per nuovi comandi.

## [5.18.0] - 2026-07-27

Sezione macchine di `/location`: dati MQTT + modifica + desktop remoto c4rgo-dsc.

### Aggiunto
- **Tab "Amministrazione"** per ogni macchina ([`templates/block/_mqtt_admin.html.twig`], incluso in `location/index.html.twig`): mostra i dati acquisiti via MQTT (presenza online/offline con "da N", segnale, rete, operatore, stato dati/IP, GPS, uptime 24h) come **snapshot dal DB** — mantenuto aggiornato dal worker `app:scanner:listener` — con pulsante **Aggiorna** per il refresh live on-demand. Consente la **modifica anagrafica** (Nome/Descrizione) persistita in DB. Bootstrap 3 + jQuery, coerente con la GUI esistente (`public/static/js/mqtt-admin.js`, `public/static/css/mqtt-admin.css`).
- **`MachineAdminController`** (`/api/v1/machine/{id}/...`, `IsGranted('ROLE_USER')` + ownership per azienda): `mqtt-summary` (GET, snapshot DB), `refresh` (POST, snapshot + ping live), `anagrafica` (POST, whitelist Nome/Descrizione), `device-config` (POST, **push generico** al device via MQTT: `{service,type,payload}` con `service` in whitelist).
- **`ScannerCommandService`**: publisher MQTT a connessione breve (connect→publish→attesa response→disconnect), envelope UUID-correlato come `ScannerMqttListenerCommand::sendCommand`, per refresh live e push config dal web.
- **Modulo remote-desktop c4rgo-dsc**: `RemoteDesktopController` (`/{_locale}/machine/{id}/remote`) conia una web-session monouso sul relay (`POST /api/v1/web-sessions`, `X-API-Key`, cURL) e rende `templates/remote/desktop.html.twig` con `<div data-c4rgo-dsc>` + viewer IIFE (`public/vendor/c4rgo/c4rgo-dsc-viewer.iife.js`). Pulsante "Desktop remoto" visibile solo se la macchina ha `dsc_device_id`.
- **`macchine.dsc_device_id`** (nullable): identità del device sul relay c4rgo-dsc (assegnata all'enrollment, distinta dal `mqtt_device_id`). Schema in `docker/mqtt/ensure-mqtt-schema.sql` (ALTER idempotente).
- Config relay in `.env` (`C4RGO_DSC_RELAY`, `C4RGO_DSC_API_KEY` → valori reali in `.env.local`) + parametri/bind in `services.yaml`.

### Note di deploy
- Applica `docker/mqtt/ensure-mqtt-schema.sql` (aggiunge `dsc_device_id`), poi `cache:clear`.
- Imposta `C4RGO_DSC_API_KEY` (e `C4RGO_DSC_RELAY`) in `.env.local` per abilitare il desktop remoto; valorizza `macchine.dsc_device_id` per i device enrollati.
- I `type` MQTT esatti per il push di configurazione dei daemon (network/wifi/mobile) vanno confermati contro c4rgo-network: l'endpoint è un pass-through generico, la mappatura dei `type` è lato client.

## [5.17.0] - 2026-07-27

L'attivazione genera il **seed SQL** con le righe di questo device (Fase 3).

### Aggiunto
- **`ApiController::buildMachineSeed()`**: la risposta di `/reg` include ora un campo `machine_sql` (best-effort) con lo script che il device applica al DB locale dopo averlo creato dal modello. Contiene, in ordine FK-safe: `INSERT` del `luoghi` di questo device, poi della `macchine`, preceduti da `DELETE` figlio→padre per essere idempotente ai retry del runner. Le righe sono ricostruite via DBAL (`SELECT *`) dallo stesso schema `c4rgoweb`, quindi combaciano con lo schema del device.
- **Allineamento `decode`**: il seed aggiorna `decode.decvalue` del record `decclass='INFO'` / `deckey='serial.number'` al `numero_seriale` della macchina, così la pipeline C++ sul device espone il serial corretto.
- Il seed è **best-effort**: un errore nella generazione viene loggato ma non invalida l'attivazione (il device gestisce l'assenza di `machine_sql` come no-op).

### Note
- Presuppone che nel modello locale `c4rgoweb.sql` le tabelle `luoghi`/`macchine` (+ operative) siano **vuote** e `aziende`/`modelli` **tenute** come riferimento: vedi il commit gemello nel repo `c4rgo` (svuotamento del modello).

## [5.16.0] - 2026-07-27

L'attivazione allinea l'identità MQTT dello scanner al suo hardware.

### Aggiunto
- **`macchine.mqtt_device_id = cpu_serial`** impostato in `recordActivation()` (`ApiController`) a ogni attivazione andata a buon fine. Il `cpu_serial` (seriale CPU del Raspberry Pi, hex minuscolo) diventa l'identità stabile dello scanner: coincide col `device_id` usato nei topic MQTT `c4rgo/{device_id}/…` e con la presenza tracciata in [5.15.0]. Idempotente: ri-attivare backfilla anche macchine già registrate. Lato device, `c4rgo-activate` scrive lo stesso valore in `device.conf` (repo c4rgo-reg-ui).

## [5.15.0] - 2026-07-23

Il worker **persiste** la presenza degli scanner: online/offline nel DB + storico delle transizioni.

### Aggiunto
- **`app:scanner:listener` sottoscrive `c4rgo/+/status/bridge`** e aggiorna la presenza di ogni scanner. Il payload è `1`/`0` (non JSON, quindi gestito prima del `json_decode` che lo scarterebbe): `1` alla connessione del bridge, `0` come will del broker su disconnessione.
- **`macchine.mqtt_online` / `macchine.mqtt_presence_at`**: stato di reachability corrente e da quando dura (aggiornato solo sulle transizioni → "offline da N"). Slegato dal modem: è la presenza del bridge, indipendente dal link (ethernet/wifi/modem), non l'heartbeat del modem che sullo scanner è solo un accesso di backup.
- **Tabella `device_presence_log`** (`App\Entity\DevicePresenceLog`): una riga per **cambio** di stato (macchina, device_id, online, changed_at). Il topic è retained, quindi alla (ri)sottoscrizione arriva lo stato corrente una volta sola e poi solo i cambi: confrontando con l'ultimo stato noto si evitano righe duplicate ai riavvii del worker. Serve a rispondere a "questo scanner era raggiungibile alle 14:32?" e a misurare l'uptime.
- Schema in `docker/mqtt/ensure-mqtt-schema.sql` (idempotente, niente Doctrine migrations): due `ALTER TABLE macchine` e la `CREATE TABLE IF NOT EXISTS device_presence_log`.

### Note di deploy
- Applica lo schema prima di far girare il worker: `mysql … < docker/mqtt/ensure-mqtt-schema.sql`, poi `cache:clear`.

## [5.14.2] - 2026-07-23

### Corretto
- **`c4rgo-mqtt-status.sh` moriva in silenzio invece di diagnosticare.** Girava con `set -e`, ma è uno strumento diagnostico che chiama comandi che falliscono di proposito (`mosquitto_sub -W` va in timeout con exit 27) o quando c'è davvero un problema (auth, connessione). Ogni assegnazione `x="$(comando)"` che tornava non-zero uccideva lo script — spesso **proprio nel ramo che doveva stampare l'errore** (la sonda `mosquitto_pub`), lasciando solo la riga d'intestazione e nessuna spiegazione. Rimosso `set -e` (tenuto `set -u`): ora il tool attraversa i fallimenti e li **riporta** (es. `✗ Connessione/credenziali: Connection Refused: not authorised`), che è il suo mestiere.

## [5.14.1] - 2026-07-23

### Corretto
- **`c4rgo-mqtt-status.sh scanners` non trovava più i topic su broker con mosquitto-clients più vecchi.** L'inventario era passato a `mosquitto_sub -F '%t'`, ma `-F` non è disponibile ovunque e `msub` ne inghiottiva lo stderr, così l'errore si travestiva da "nessun topic / credenziali sbagliate". Tornato a `-v` (portabile) filtrando le sole righe `^c4rgo/`, che scarta anche le continuazioni dei payload JSON multiriga.
- **Diagnosi reale quando l'elenco è vuoto**: una sonda `mosquitto_pub -n` distingue "connessione/auth fallita" (mostra l'errore vero) da "connesso ma nessuno scanner ha ancora pubblicato", invece di dare sempre la colpa alle credenziali.

## [5.14.0] - 2026-07-23

Segnale di **presenza** degli scanner (online/offline) sul cloud, indipendente dal link.

### Aggiunto
- **Presenza del device via Last Will del bridge.** Il bridge di ogni scanner ora pubblica sul broker cloud un topic retained `c4rgo/<device>/status/bridge`: `1` alla connessione, `0` alla caduta — quest'ultimo registrato come **will** e pubblicato dal broker stesso, quindi scatta su crash, mancanza corrente, reboot e stop pulito (su blackhole di rete dopo il keepalive, ~45s). Bastano due righe di config nel bridge (`notifications true` + `notification_topic`, quest'ultimo per spostare la notifica dal default `$SYS` — non leggibile dall'ACL del device — al nostro namespace); **zero codice** sul device, due messaggi per transizione. Verificato end-to-end in lab su mosquitto 2.0.21.

### Motivazione
- Il segnale di presenza precedente (freschezza dell'heartbeat del modem) era il sensore sbagliato: sugli scanner l'accesso primario è **ethernet/wifi** e il modem è un **backup**, spesso in standby. Legare "lo scanner è raggiungibile" al daemon del modem è fragile e semanticamente errato. La presenza del bridge è invece indipendente dal link: vale via ethernet, wifi o modem-fallback.

### Modificato
- **`c4rgo-mqtt-status.sh` `scanners`**: la colonna `STATO` ora legge il topic di presenza (online/**OFFLINE**/`?` per i device legacy senza `notification_topic`) invece di dedurre la liveness da una finestra d'ascolto di 35s sull'heartbeat. Autorevole, istantaneo e molto più veloce; rimossa l'opzione `-w`. L'inventario dei topic usa `-F '%t'` (solo nomi) per non rompersi su payload JSON multiriga.
- `device-bridge.conf.example`: aggiunto `notification_topic` con la spiegazione del meccanismo. La notifica è pubblicata direttamente sul remoto, **non** serve una riga `topic`.

## [5.13.0] - 2026-07-23

Strumenti per gestire il canale MQTT senza essere sistemisti: una checklist e un cruscotto a colpo d'occhio.

### Aggiunto
- **[`deploy/mqtt-baremetal/CHECKLIST.md`](deploy/mqtt-baremetal/CHECKLIST.md)**: lista della spesa copia-incolla per configurare il canale. Cloud (test/prod) e scanner separati, con la sola differenza test↔prod evidenziata (`EXTRA_SAN` sul broker, due righe sul device). Rimanda a TEST-LAB/DEPLOY per i *perché*, ma il percorso felice sta tutto qui.
- **[`deploy/mqtt-baremetal/c4rgo-mqtt-status.sh`](deploy/mqtt-baremetal/c4rgo-mqtt-status.sh)**: un solo comando che capisce da solo se gira sul broker o su uno scanner.
  - `scanners` (default, lato cloud): elenca i device e distingue **online** (heartbeat non-retained negli ultimi 35s = davvero connesso ora) da **silente** (solo dati retained), con numero di topic e servizi pubblicati. Sfrutta i due segnali dei daemon — heartbeat ogni 30s non-retained, `modem/info` ogni 60s retained — invece di leggere il DB.
  - `doctor`: check di configurazione con OK/attenzione/errore. Sul broker verifica listener 8883/locale, SAN e scadenza del cert, `passwd`/`acl`, login `r_server` e client connessi. Sullo scanner verifica il bridge dal conf e ne conferma la connessione **a livello TCP uscente verso il cloud** (`ss`), segnale cred-free più affidabile del topic `$SYS` locale, che sul device richiede autenticazione.
  - `watch <device>`: tail live dei topic di un device.

## [5.12.0] - 2026-07-21

### Corretto
- **Il bridge dei device ponticellava l'intero bus locale: −98,6% di traffico.** `topic c4rgo/<device>/# both 1` spediva al cloud ogni messaggio dei daemon, mentre il worker ne sottoscrive cinque pattern. Misurato sul bus reale di uno scanner: **3,1 MB/giorno**, di cui il worker leggeva l'1,5% — da solo `mon/services`, che nessuno consuma, valeva il 60%. Sostituito con l'elenco esplicito dei topic consumati: **0,044 MB/giorno**, cioè da ~93 a ~1,3 MB/mese di SIM per device. Il canale nasce per scanner su rete mobile, dove il costo è ricorrente e moltiplicato per la flotta.
- **Il worker non riceveva gli alert di `mon`.** Sottoscriveva `c4rgo/+/+/events`, che ha esattamente quattro livelli, ma `c4rgo-mon` pubblica su `mon/events/alert` e `mon/events/crash` (cinque). Disco pieno, sovratemperatura e crash di servizi non sarebbero mai arrivati al cloud, in silenzio. Corretto in `c4rgo/+/+/events/#`, che in MQTT include anche il livello padre e copre entrambe le forme usate dai daemon.

> Chi ha già installato un bridge con il wildcard deve sostituire la riga `topic` e cancellare i retained obsoleti rimasti sul broker cloud (`mosquitto_pub -r -n` sui topic non più ponticellati), altrimenti il cloud continua a servire dati fermi.

## [5.11.2] - 2026-07-21

### Corretto
- **Il worker `app:scanner:listener` non si fermava con Ctrl-C né con SIGTERM.** Usava `MqttClient::loop(true, true, 1000)`, ma in php-mqtt/client la condizione d'uscita è `$exitWhenQueuesEmpty && countSubscriptions() === 0`: con cinque subscription attive quel blocco non viene mai valutato, quindi né lo svuotamento delle code né il `waitLimit` possono interromperlo e `loop()` non restituisce mai il controllo. I gestori di segnale impostavano `$running = false` su un flag che nessuno rileggeva più. Sostituito con `loopOnce()`, che lascia il ciclo al comando.
- **Conseguenza dello stesso bug: il `$em->clear()` periodico era codice morto.** Il contatore avanzava solo al ritorno di `loop()`, cioè mai, quindi l'UnitOfWork cresceva senza limite — esattamente la perdita di memoria che quel blocco doveva impedire. Ora il giro è nostro e il clear avviene circa ogni 20 secondi.
- Il commento `waitLimit ms` era doppiamente errato: il parametro è in **secondi**, ed era comunque irraggiungibile.

> Senza questa correzione la unit systemd si sarebbe fermata solo per `SIGKILL` allo scadere di `TimeoutStopSec`, a ogni `stop` e a ogni `restart`.

## [5.11.1] - 2026-07-21

### Corretto
- **`device-bridge.conf.example`: `topic ... both 0` → `both 1`.** Con QoS 0 un comando cloud→device perso sulla rete mobile non viene mai ritentato; il canale nasce per inviare comandi, quindi serve at-least-once.
- **Documentata la trappola dei commenti a fine riga in mosquitto**, verificata in laboratorio: il `#` è un commento solo a inizio riga, e su `remote_password` il parser prende tutto il resto della riga. Un commento in coda finisce nella password e il bridge fallisce con `Connection Refused: not authorised` pur avendo credenziali corrette — mentre `mosquitto_sub` con le stesse credenziali si collega, il che manda fuori strada. Su `address` e sui booleani l'errore passa inosservato perché viene usato solo il primo token. I commenti in coda già presenti nell'esempio sono stati spostati su righe proprie.

## [5.11.0] - 2026-07-21

Il kit bare-metal ora copre anche una **macchina di test in LAN**, con un delta di una riga sola verso la produzione.

### Aggiunto
- **[`deploy/mqtt-baremetal/TEST-LAB.md`](deploy/mqtt-baremetal/TEST-LAB.md)**: runbook per replicare il canale MQTT su un PC di test. Il certificato del broker di test porta `DNS:my.c4rgo.cloud` **più** `IP:<ip-test>` fra le SAN, così il bridge dei device valida il certificato anche puntando all'IP (`bridge_insecure false` resta attivo) e il passaggio test→prod cambia solo la riga `address`. Documenta perché l'alternativa `/etc/hosts` è sbagliata: dirotterebbe anche l'HTTPS verso `my.c4rgo.cloud` (registrazione, `c4rgo-mon`, polling `comandi_exec`).
- `install-broker-baremetal.sh` parametrizzato via ambiente — `EXTRA_SAN` (SAN aggiuntive), `LOCAL_PORT` (listener locale, se la 1883 è già occupata da un altro broker sulla macchina di sviluppo), `BROKER_CN` — così **lo stesso script** installa test e produzione. La conf `conf.d` viene sostituita al volo su CN e porta locale, e se il certificato esiste già lo script stampa le SAN presenti invece di lasciar credere di averle aggiornate.

## [5.10.0] - 2026-07-16

Canale MQTT **scanner ⇆ cloud**: broker centrale + worker che consuma la telemetria degli scanner 3D e può inviare comandi, indipendentemente dal servizio web.

> ⚠️ **Deploy**: NON solo codice. Richiede (1) colonna DB `macchine.mqtt_device_id` (script idempotente, il progetto non usa migrations), (2) certificati broker firmati dalla C4rgo-CA, (3) `SCANNER_MQTT_PASSWORD` in `.env.local`, (4) due nuovi servizi compose (`mqtt`, `scanner-listener`). Runbook completo: [`docker/mqtt/DEPLOY.md`](docker/mqtt/DEPLOY.md).

### Aggiunto
- **Broker MQTT centrale** (`docker/mqtt/`): `mosquitto.conf` (listener TLS 8883 per i device + 1883 interno alla rete docker per il worker), `acl` (isolamento per-device via `pattern readwrite c4rgo/%u/#`, ruolo `r_server` full-access), `setup-broker.sh` (cert firmato dalla CA + `passwd`), `device-bridge.conf.example` per il bridge outbound sui Raspberry, `README.md` (razionale) e `DEPLOY.md` (runbook).
- **Worker `app:scanner:listener`** ([`ScannerMqttListenerCommand`](src/Command/ScannerMqttListenerCommand.php)): sottoscrive event/response degli scanner, mappa `mqttDeviceId`→`Macchine`, e `sendCommand()` gestisce entrambi i formati comando (`CommandEnvelope` vs flat `barcode`) e le 3 chiavi di correlazione (`command_id`/`id`/`request_id`). Servizio compose `scanner-listener` (`restart: always`).
- **`ensure-mqtt-schema.sql`**: garantisce `macchine.mqtt_device_id` (idempotente, MySQL 8 + MariaDB).
- Parametri `scanner_mqtt_*` (`config/services.yaml`) e blocco `SCANNER_MQTT_*` in `.env` (template, segreti in `.env.local`), indipendenti dal legacy `MQTT_*`.
- **Kit deploy BARE-METAL** (`deploy/mqtt-baremetal/`) per il server online che gira senza docker (nginx+php-fpm di sistema): `DEPLOY-baremetal.md`, `mosquitto-c4rgo-scanner.conf` (conf.d di sistema, listener 1883 su 127.0.0.1), `install-broker-baremetal.sh` (mosquitto di sistema + certs + passwd/acl), `c4rgo-scanner-listener.service` (unit systemd, `Restart=always`, stop pulito via SIGTERM). Il worker sull'host punta a `SCANNER_MQTT_HOST=127.0.0.1` (override in `.env.local`).

### Corretto
- **`mosquitto.conf`: il listener interno 1883 era bindato su `127.0.0.1`** → il worker, che gira in un container separato e raggiunge il broker come `mqtt:1883`, riceveva *connection refused*. Ora `listener 1883 0.0.0.0` (resta interno: il compose pubblica solo 8883).
- **`setup-broker.sh` generava la password di `r_server` con `openssl rand` inline senza mai stamparla** → impossibile popolare `.env.local`. Ora la cattura e la stampa.

### Deprecato
- Canale legacy SIM7600 (`Sim7600MqttListenerCommand`, `SIM7600Service`, broker `MQTT_*`): marcati `@deprecated`, sostituiti dal canale scanner sopra. Il modem è ora gestito dal daemon `c4rgo-modem`.

## [5.9.2] - 2026-07-13

Fix del *fatal error* per memoria esaurita nella ricerca acquisizioni su intervalli ampi.

> ✅ **Deploy**: solo codice PHP. Nessuna modifica di schema. Aggiornare + `cache:clear`/`cache:warmup`.

### Corretto
- **`GET /{_locale}/searchresult/{data}/{p}` andava in HTTP 500** (`Allowed memory size of 134217728 bytes exhausted`) su ricerche ampie, es. `LY` = *anno scorso*. `SearchController::searchResultEx()` costruisce la tabella temporanea `tmp_search` con `SELECT s.*` da una vista di **106 colonne** più i campi di 7 JOIN (122 colonne in tutto), poi la rileggeva **interamente in memoria** con `SELECT * ... fetchAllAssociative()`. Con le 12.374 righe del 2025 il picco toccava ~134 MB contro il `memory_limit` di 128 MB. Ora la query **proietta solo le 50 colonne effettivamente usate** dal ciclo e il result set viene **scorso in streaming** (`iterateAssociative()`), senza mai materializzarlo. Misurato: picco da ~134 MB a ~50 MB sulla stessa ricerca; sul caso peggiore (tutte le righe) da ~248 MB a ~84 MB.
- Il `prev`/`next` di navigazione, prima calcolato dentro il ciclo accedendo alle righe vicine (`$result[$c-1]` / `$result[$c+1]`) — impossibile in streaming — viene ora ricostruito dopo il ciclo dall'elenco ordinato degli `awb`. Verificata l'equivalenza con l'algoritmo originale sui dati reali (12.374 e 24.482 righe) e sui casi limite (0, 1, 2, 3 righe e `awb` duplicati): identico.

### Note
- **Resta aperto il filtro data `A` (*All*, nessun filtro sull'anno)**, che oggi significa 24.482 righe e ~14 MB di JSON: continua a superare i 128 MB in ambiente `dev`. Non è una regressione — falliva anche prima, e peggio (~248 MB) — ma il fix da solo non basta: quella ricerca va risolta con la **paginazione lato server**, non caricando l'intero risultato in un colpo. Verificato via HTTP reale: `LY/A` passa da 500 a **200** (12.374 righe), `A/A` resta 500.
- **Lo stesso schema `SELECT * FROM tmp_search` + `fetchAllAssociative()` è presente in altri 5 punti** non toccati da questo fix, con lo stesso rischio: `SearchController::searchResultAcqEx()`, `SearchDeletedController` (2), `ListController` (1), `Admin\SearchErpController` (2).

## [5.9.1] - 2026-07-13

Aggiornamento delle dipendenze (solo `composer.lock`, nessun vincolo cambiato in `composer.json`).

> ⚠️ **Deploy**: richiede `composer install --no-dev` (il lock è cambiato), poi `cache:clear` + `cache:warmup`. Nessuna modifica di schema.

### Cambiato
- **44 pacchetti aggiornati, nessuno aggiunto o rimosso.** In prevalenza i componenti Symfony da 6.4.32–6.4.41 a **6.4.42** (resta la 6.4 LTS: nessun cambio di major, nessun aggiornamento del framework).
- `doctrine/lexer` 2.1.1 → **3.0.1**: unico cambio di major, richiesto da Doctrine. Verificato che `doctrine:schema:validate` resti pulito.
- `symfony/deprecation-contracts` v3.7.0 → **v2.5.0**: è un *downgrade*, ma innocuo — tutti i pacchetti che lo usano accettano `^2.1 || ^3` e l'API (`trigger_deprecation()`) è la stessa.

### Note
- Verificato dopo l'aggiornamento: `composer validate` senza nuovi avvisi, `vendor/` allineato al lock, container compilato, mapping Doctrine corretti, suite di test verde (31 test).

## [5.9.0] - 2026-07-13

Selettore del tipo di vista sulle mappe: la vista non è più cablata a satellite.

> ✅ **Deploy**: solo asset statico (`public/static/js/`), nessun build Webpack né modifica di schema. Utile un hard refresh per invalidare la cache del browser.

### Aggiunto
- **Pannellino di scelta della vista su ogni mappa** (`L.control.layers` nativo di Leaflet, in alto a destra), con quattro basemap: **Satellite** (Esri World Imagery, il default storico), **Stradale** (OpenStreetMap), **Scura** (CARTO dark) e **Topografica** (OpenTopoMap). Nessuna libreria aggiuntiva.
- **La scelta è ricordata** nel `localStorage` del browser (chiave `c4rgo.maps.view`): vale per tutte le mappe dell'app e sopravvive al ricaricamento. Dove il `localStorage` non è accessibile (es. Safari in navigazione privata) si degrada in silenzio al default satellite.
- **Le mappe della stessa pagina restano allineate**: cambiando vista su una, le altre la seguono — utile in `location/`, dove convivono la mappa dei luoghi e una mappa per macchina.

### Cambiato
- `maps-provider.js`: le basemap sono ora in un registro (`BASEMAPS`) invece che in una catena di `if` su `mapTypeId`. Aggiungere una vista = aggiungere una voce. Nuova API `C4Maps.getView()`.
- Con provider Google Maps abilitato il `mapTypeControl` nativo, equivalente del selettore Leaflet (prima era disattivato).
- `mapTypeId: 'hybrid'` passato da `googlemaps.js` resta valido per Google; su Leaflet la vista iniziale viene ora dalla preferenza salvata, con satellite come default — quindi **il comportamento a prima apertura non cambia**.

## [5.8.4] - 2026-07-13

Fix delle mappe delle macchine in `location/`, che si vedevano come un riquadro grigio con una sola tile nell'angolo. La mappa principale dei luoghi era corretta.

> ✅ **Deploy**: solo asset statico (`public/static/js/`), nessun build Webpack né modifica di schema. Basta aggiornare il codice; utile un hard refresh per invalidare la cache del browser.

### Corretto
- **Mappe delle macchine (Leaflet) rotte nella pagina `location`.** `initMap()` crea tutte le mappe su `$(document).ready`, ma i `.map-track` delle macchine vivono dentro due pannelli Bootstrap annidati (luogo → macchina) che partono **collassati**: al momento della creazione il contenitore è `display:none`, quindi Leaflet misura una viewport di 0×0. All'apertura del pannello nessuno lo avvisava che ora aveva spazio, così restava convinto di essere 0×0: caricava una manciata di tile e le posizionava con offset assurdi (`translate3d(997.999px, 273px, …)`). La mappa principale dei luoghi non ne soffriva perché è sempre visibile. Ora le mappe create vengono registrate e, all'evento `shown.bs.collapse`, ridimensionate (`C4Maps.triggerResize()` → `invalidateSize()`) e ricentrate. Verificato in Chrome headless sulla struttura reale della pagina: prima 1 tile su un container 600×300, dopo 9 tile e mappa centrata sul marker.

### Note
- `C4Maps.triggerResize()` esisteva già nell'adapter (`maps-provider.js`) ma non era mai invocato: il fix aggancia solo il momento giusto per chiamarlo.

## [5.8.3] - 2026-07-13

Correzioni emerse dal log del web server in sviluppo: piattaforma DBAL sbagliata (il DB è MariaDB, DBAL usava MySQL 5.6), due deprecation eliminabili e una texture 3D non trovata.

> ✅ **Deploy**: nessuna modifica di schema richiesta dal codice. Basta aggiornare + `cache:clear`/`cache:warmup`.
>
> ⚠️ **Da verificare in produzione**: la nuova chiave `DATABASE_SERVER_VERSION` (default `10.11.14-MariaDB`) va allineata in `.env.local` alla versione reale del server di produzione — dichiararne una più recente di quella effettiva farebbe generare a DBAL SQL non supportato.
>
> ℹ️ **Nota su `schema:update`**: con la piattaforma corretta l'anteprima `--dump-sql` cambia. Spariscono ~76 `ALTER TABLE` fantasma (erano artefatti della piattaforma MySQL 5.6), ma compaiono le conversioni `LONGTEXT` → `JSON` delle colonne `DC2Type:json`: su MariaDB `JSON` è un alias di `LONGTEXT` + `CHECK (json_valid(...))`, quindi sono allineamenti di metadati, **non** una modifica dei dati, e DBAL 3.10 le reintrospetta correttamente (nessun diff perpetuo). Restano comunque **facoltative**: come sempre su questo DB, non eseguire `schema:update`/`migrate` alla cieca.

### Corretto
- **Piattaforma DBAL sbagliata: MySQL 5.6 al posto di MariaDB.** `config/packages/doctrine.yaml` dichiarava `server_version: '3.15'` — un residuo del default SQLite dello skeleton Symfony — mentre `DATABASE_URL` usa il driver `mysqli`. DBAL interpretava `3.15` come una versione MySQL antichissima e istanziava `MySQLPlatform`, emettendo a ogni richiesta la deprecation *“MySQL 5.6 support is deprecated”* e generando SQL per la piattaforma sbagliata. Ora `server_version` viene dalla nuova env `DATABASE_SERVER_VERSION` (default `10.11.14-MariaDB`) e DBAL usa `MariaDb1010Platform`, coerente col DB reale.
- **Texture 3D dell'avatar non trovata (404 a ogni apertura del configuratore).** In `AVATAR-CONSITE-LIGHT.mtl` il materiale `Mesh_FRONT_214614` puntava a `/Users/macproben/Library/Application Support/CLO/…/M02-E_Nylon (See-Through)_DESATURATION_214624.png`: un path assoluto della macchina di chi ha esportato il modello, lasciato lì dall'esportatore CLO. Il browser lo risolveva come URL relativo (`/static/cloud.js/models/obj//Users/…`) → 404 e materiale senza texture. Corretto nel path relativo `AVATAR-CONSITE-LIGHT/M02-E_Nylon (See-Through).png`, file già presente nel repo, coerente con tutti gli altri materiali dello stesso `.mtl`.

### Cambiato
- **Deprecation Liip ImagineBundle azzerata**: `liip_imagine.twig.mode: lazy`. `FilterExtension`/`FilterTrait` sono deprecati dalla 2.7 e rimossi nella 3.0; il runtime lazy espone lo stesso filtro `|imagine_filter` (che peraltro nessun template usa oggi).
- **Deprecation ORM 3 sulle nostre entità azzerata**: abilitato `doctrine.orm.report_fields_where_declared: true`, ora possibile perché i mapping `inversedBy`/`mappedBy` invalidi che lo impedivano sono stati corretti nella 5.8.0. Verificato: `schema:validate` resta pulito e il diff di schema è invariato. Resta emessa la stessa deprecation dal bundle vendor `dukecity/command-scheduler-bundle`, che registra un proprio `AttributeDriver` senza il flag: non correggibile lato applicazione.

### Note
- **`underscore_number_aware` non è adottabile** (deprecation ORM 3 che resta aperta): la naming strategy number-aware rinominerebbe tutte le colonne legacy con cifre senza `name:` esplicito (`iata3digit` → `iata_3_digit`, `ibx1` → `ibx_1`, …). Verificato sul campo: `schema:update` aborta con *“There is no column with name iata3digit on table vettori”*. Va sciolto in Fase 3 fissando i `name:` espliciti nelle `#[ORM\Column]` interessate.

## [5.8.2] - 2026-07-09

Fix dell'URL dell'immagine principale dei contenitori in due viste che usavano ancora il vecchio percorso (solo nome file), mentre l'edit usava già `/fmuploads/contenitori/`.

> ✅ **Deploy**: nessuna modifica di schema. Basta aggiornare il codice + `cache:clear`/`cache:warmup` (la modifica a `ImageGen` è PHP compilato).

### Corretto
- **Lista contenitori (`contenitori/index`)**: la colonna immagine della DataTable usava `row.ImmaginePrincipale` (solo il nome file), risolto dal browser come URL relativo alla pagina → immagine non trovata. Ora usa `row.ImmaginePrincipaleUrl`, già esposto dall'endpoint `api_contenitore_get` con il path completo `/fmuploads/contenitori/…` (coerente con la pagina di edit).
- **Configuratore 3D C4rgo (`configuratore3d/c4rgo/…`)**: le immagini dei contenitori generate da `ImageGen::getImage($file, 3)` renderizzavano `block/img_found.html.twig` con il solo nome file, producendo il vecchio URL. Anteposto `/fmuploads/contenitori/` in un unico punto (`case 3`), usato esclusivamente per l'immagine principale dei contenitori (scena 3D e lista *saved items*).

## [5.8.1] - 2026-07-02

Fix dell'endpoint di registrazione device dell'API REST, che falliva con `SERVER_ERROR` per un alias Doctrine non più supportato.

> ✅ **Deploy**: nessuna modifica di schema. Basta aggiornare il codice + `cache:clear`/`cache:warmup`.

### Corretto
- **`POST /api/v1/reg` (registrazione DS-C4RGO) restituiva sempre `SERVER_ERROR`.** `ApiController::findMachine()`/`findMachineByHardware()` usavano l'alias corto `getRepository('App:Macchine')`, non più supportato da `doctrine/persistence:3.x` (lanciava eccezione, mascherata dal `catch` generico in `SERVER_ERROR`). Sostituito con `getRepository(Macchine::class)`.
- **Stesso alias corto latente in due query DQL** (avrebbero rotto le rispettive query allo stesso modo): `AddressBookRepository::findByUser()` (`innerJoin('App:User', ...)` → `User::class`) e `VolumeRepository::findWithSessionValid()` (`innerJoin('App:Awbexport', ...)` → `Awbexport::class`).

## [5.8.0] - 2026-06-26

Riscrittura in Symfony del servizio di **upload acquisizioni** (ex `public/upload.php`), con salvataggio su filesystem e aggiornamento DB disaccoppiati, e nuovo **log delle operazioni** a ritenzione limitata.

> ⚠️ **Deploy**: modifica di schema → nuova tabella `acquisition_upload_log`. Modo consigliato (chirurgico): eseguire **solo** questa migrazione `doctrine:migrations:execute 'DoctrineMigrations\Version20260625120000' --up`, poi `cache:clear` + `cache:warmup`. La migrazione usa `CREATE TABLE IF NOT EXISTS`, quindi è rieseguibile senza errori. In alternativa `doctrine:schema:update --force` ora funziona di nuovo (i mapping legacy che lo facevano abortire sono corretti in questa release — vedi *Corretto*); usarlo **solo** dopo aver verificato l'anteprima `--dump-sql` (mai `--complete`). La chiave `decode` `CONFIG/base.data.folder` deve puntare a una cartella scrivibile. Le altre chiavi sono opzionali (vedi sotto): senza di esse valgono i default. La nuova rotta è `PUBLIC_ACCESS` (upload da macchina, senza login), come il vecchio `upload.php`.

### Aggiunto
- **Servizio Symfony di upload acquisizioni**: `POST /api/v1/acquisition/upload?id={macchina}&uid={utente}` (campo file multipart `c4rgofile`). Riceve uno zip, lo estrae nella cartella configurata e — **solo se abilitato** — aggiorna il DB (`session`/`weight`/`volume`/`awbexport`/`barcode`). Il **salvataggio su filesystem avviene sempre**, l'import DB è opzionale e separato. Query interamente **parametrizzate** (eliminata la SQL injection del vecchio script) e niente più credenziali DB hardcoded: usa la connessione Doctrine dell'app.
- **Log delle operazioni** (`acquisition_upload_log`): una riga per ogni record (`sid`) elaborato — ip del chiamante, `sid`, `machine_id`/`user_id`, nome file, flag *db aggiornato*, esito (`OK`/`ERROR`) e messaggio. **Ritenzione configurabile (default ~1 mese)**: a ogni inserimento le righe più vecchie della soglia vengono cancellate, **senza schedulatore**.
- **Parametri configurabili da tabella `decode`** (classe `CONFIG`): `base.data.folder` (radice di salvataggio, già esistente), `base.data.dbimport.enabled` (flag aggiornamento DB, default `0`/off), `base.data.upload.maxbytes` (default 10000000), `base.data.upload.field` (default `c4rgofile`), `base.data.log.retention.days` (default 30).

### Cambiato
- `config/services.yaml`: bind `$defaultRootFolder` come fallback di `base.data.folder`.
- `config/packages/security.yaml`: `^/api/v1/acquisition/upload` esposto come `PUBLIC_ACCESS`.

### Corretto
- **`doctrine:schema:update` di nuovo utilizzabile** (`doctrine:schema:validate` ora pulito). Allineati al DB i mapping legacy che facevano abortire il comando / lo segnalavano come invalido:
  - `Traduzioni`: `UniqueConstraint` IDX01 usava colonne inesistenti (`nometermine`/`nomearea`) → corrette in `nome_termine`/`nome_area` (era l'errore bloccante `schema:update`).
  - `Devices↔DeviceDecode`, `Modelli↔Macchine`, `Macchine↔GPS`: `mappedBy` del lato inverso puntava a campi inesistenti → allineati ai campi owning reali (`DDDDevice`, `Modello`, `Macchina`); corretti anche i relativi helper `add*/remove*` che chiamavano setter dal nome vecchio.
  - `Contenitori#Produttore`/`#Fornitore`: `inversedBy` verso campi inesistenti su `Aziende` → rese unidirezionali (nessuna modifica alla join table).
  - Rimosse due associazioni morte e mai usate: `Montaggi#Componente` (OneToMany verso un campo owning inesistente; la relazione reale è la ManyToMany `Componenti`) e `User#scheduledCommands` (ManyToMany verso un'entità vendor priva di lato inverso).
  - Nessuna di queste correzioni modifica lo schema del DB (solo metadata dei lati inversi).
- **DataTables SA — `InputBag::get()` con valore array** (SF6): gli endpoint `*/data` dei CRUD superadmin (`Aziende`, `Comandi`, `Componenti`, `FamigliaComponenti`, `Luoghi`, `Macchine`, `Modelli`, `Montaggi`) leggevano `search`/`order` con `$request->query->get('search', [])`, che in Symfony 6 lancia *“Expected a scalar value as a 2nd argument to InputBag::get()”*. Sostituito con `$request->query->all('search')` / `->all('order')` (i parametri array vanno letti con `all()`).

### Note
- Il legacy `public/upload.php` resta nel repo per compatibilità: va **dismesso** quando tutte le macchine puntano al nuovo endpoint.

## [5.7.0] - 2026-06-12

Oggetto email e destinatari spostati/ampliati sulle **notifiche a eventi**; nuove notifiche-eventi **globali** del superadmin (invio separato) al posto della "email in copia".

> ⚠️ **Deploy**: modifica di schema → `doctrine:schema:update --force` (o migrazione `Version20260612120000`), poi `cache:clear` + `cache:warmup`.

### Aggiunto
- **Oggetto email** configurabile sulle notifiche-eventi, personalizzabile con i soli tag "inline" (testo: `{{codice_documento}}`, `{{data}}`, ecc.); i tag che generano tabelle/immagini/blocchi non sono ammessi nell'oggetto. Se vuoto si usa l'oggetto di default.
- **Rubrica destinatari**: oltre agli utenti, si possono selezionare le voci della rubrica (`address_book`) dell'azienda (ManyToMany).
- **Notifiche-eventi globali del superadmin** (`/sa/notifiche-eventi`, link in SA → Dashboard): i destinatari qui configurati ricevono, in un **invio separato** e con oggetto proprio, gli eventi di **qualsiasi** azienda. Sostituisce la vecchia "email in copia".
- **Tipo evento "Supporto"**: anche le richieste di supporto/intervento passano ora dalle notifiche-eventi (per-azienda + globale), non più solo dall'email di support.

### Cambiato
- Invio email evento: ora **due email separate** — una ai destinatari dell'azienda (con il suo oggetto), una ai destinatari globali del superadmin (con il suo).
- **email-template**: rimossi dal form i campi **Oggetto** ed **Email in copia** (l'oggetto è sulle notifiche-eventi; la copia è sostituita dalle notifiche-eventi globali). Colonne mantenute in DB ma inutilizzate.

## [5.6.0] - 2026-06-12

Template email riservati al superadmin e ridisegnati come **blocchi nominati riutilizzabili** iniettabili nei `.twig`.

> ⚠️ **Deploy**: solo codice, nessuna modifica di schema. Dopo l'aggiornamento: `cache:clear` + `cache:warmup`.

### Cambiato
- **Template email solo SUPERADMIN**: route `/admin/email-template` → `/sa/email-template`. Rimossi i link dal pannello admin; resta solo quello in SA → Dashboard.
- **Flag "Sovrascrivibile" rimosso dal form** e forzato sempre spento: per ora le aziende non possono sovrascrivere i template globali. Campo entità e logica del renderer conservati per riattivarlo.
- **"Tipo" ora è un nome libero** (identificatore Twig, univoco per azienda/lingua) invece di una scelta fissa. I template diventano **blocchi nominati riutilizzabili**: nei `.twig` delle email si scrive `{{nome}}` (consigliata la convenzione `_nome_`) e viene sostituito dal blocco renderizzato. Rimosso il vecchio "il template sostituisce il corpo per tipo": il corpo è sempre lo scheletro `.twig` built-in; i blocchi completano le parti variabili. cc/bcc e immagini inline provengono dai soli blocchi effettivamente usati.

### Aggiunto
- **Alert "dato mancante"**: se un blocco usa un dato non disponibile (es. nessun codice documento per `{{barcode}}`), al suo posto compare un riquadro d'errore visibile invece di un vuoto. I segnaposto `_nome_` referenziati ma senza blocco (assente/disabilitato) vengono resi vuoti, senza errori di variabile Twig indefinita.
- Negli scheletri `emails/congruity.html.twig` e `emails/support.html.twig` aggiunti i segnaposto `{{ _footer_ }}` (e `{{ _congruity_ }}`): creando i blocchi omonimi in SA → Template email, il contenuto viene iniettato all'invio.

## [5.5.0] - 2026-06-11

Tag immagine per i template email (loghi, banner, immagini generiche) e tag barcode/QR del codice documento.

> ⚠️ **Deploy**: solo codice. Nessuna modifica di schema, nessuna nuova dipendenza (barcode/QR già presenti). Dopo l'aggiornamento: `cache:clear` + `cache:warmup`.

### Aggiunto
- **Tag immagine inline (CID)** nei template email — compatibili con tutti i client (Gmail, webmail Aruba e client desktop **anche offline**: niente URL remoti né data-URI):
  - `{{immagine:nomefile.png}}` — immagine generica dalla cartella `public/static/emails/images` (sintassi parametrica `{{nome:argomento}}`, nuova nel motore dei tag);
  - `{{logo_azienda}}` — logo dell'azienda corrente;
  - `{{logo_c4rgo}}` — logo C4RGO.
- **Tag codice documento**:
  - `{{barcode}}` — Code128 del codice documento (tc-lib-barcode);
  - `{{qrcode}}` — QR con l'URL alla pagina di dettaglio del documento (`/{locale}/detail/1/{awb}/0/0`).

Pipeline di incorporamento: i tag registrano le immagini in un collector (`App\Email\EmailImageEmbedder`), il renderer le raccoglie e l'invio (`CheckCommand`) le allega al messaggio come parti inline `cid:`.

### Risolto
- **Congruità — colore nel template email built-in** (`emails/congruity.html.twig`): allineato alla logica direzionale dei tag — rosso se il rilevato supera il previsto, verde se inferiore. Prima evidenziava in rosso qualunque scostamento significativo a prescindere dal segno.

## [5.4.0] - 2026-06-11

Miglioramenti ai **template email** (copie cc/bcc, UI) e alla **congruità** (nuova tabella di dettaglio per oggetto, fix del colore).

> ⚠️ **Deploy**: aggiunge 2 colonne a `email_template` → eseguire `doctrine:schema:update --force` (o la migrazione `Version20260611100000`). Nessun altro passo.

### Aggiunto
- **Template email — copie aggiuntive**: nuovo campo per indicare una o più email a cui inviare in copia le email generate dal template (oltre ai destinatari configurati con l'apposita interfaccia), con scelta **CC** (in copia) o **BCC** (copia nascosta).
- **Congruità — tag `{{tabella_congruita_dettaglio}}`**: tabella di dettaglio con una riga per oggetto (`obj_code`) e, in colonna, peso e volume (previsto/rilevato/differenza); dimensioni L×W×H per l'oggetto singolo, icona di avviso quando uno o più degli n oggetti ha dimensioni L×W×H non congrue.

### Cambiato
- **Form template email**: checkbox (Abilitato, Sovrascrivibile) e radio (CC/BCC) ingranditi e allineati verticalmente al testo (nella cornice in stile scheduler erano minuscoli e in basso rispetto all'etichetta).

### Risolto
- **Congruità — colore della differenza** (tag `{{tabella_congruita}}` e nuovo tag): ora è **direzionale** — rosso se il rilevato supera il previsto (consegnato più del dichiarato/pagato, a svantaggio del vettore), verde se rilevato ≤ previsto. Prima qualunque scostamento veniva colorato di rosso a prescindere dal segno.

## [5.3.0] - 2026-06-10

Iterazione su **notifiche a eventi**, **template email** ed **esperienza della dashboard di benvenuto**, più un fix alle sessioni.

> ⚠️ **Deploy**: nessuna modifica di schema. Gli asset di TinyMCE sono versionati nel repo (`public/static/tinymce/`) → arrivano col pacchetto, nessun comando di install.

### Aggiunto
- **Notifiche a eventi**: attivazione/disattivazione rapida dall'elenco (toggle di stato con la stessa icona/comportamento degli schedulatori cron).

### Cambiato
- **Schedulatore a eventi (notifiche)**: UI allineata agli schedulatori a tempo — cornice scura/trasparente, selettore destinatari a griglia di checkbox (4 colonne, nome del selezionato su sfondo verde), elenco con colonna di stato e azioni a icone. Destinatari filtrati per azienda corrente e mostrati col solo nome.
- **Template email (admin)**: lista ed editor riportati alla grafica degli schedulatori. Il corpo HTML si modifica con un **vero editor HTML self-hosted — TinyMCE community (GPL)** — al posto di CKEditor 4: nessuna API key/licenza, funziona offline. CKEditor 4 dismesso perché i release open-source non sono più scaricabili (l'upstream serve solo build LTS commerciali, con errore di licenza).
- **Dashboard di benvenuto (`secwelcome`)**: l'auto-fit adatta il contenuto al viewport (niente scroll, logo finale sempre visibile, niente "flash" al load); tile del menu uniformi (icona sopra, stessa altezza); navbar agganciata al top con testi a destra centrati verticalmente; colonna sinistra riorganizzata (blocco informativo che occupa l'altezza, menu e barra di ricerca in fondo).

### Risolto
- **Sessioni**: salvataggio spostato in `var/sessions` (di proprietà del processo PHP) invece del default di sistema `/var/lib/php/sessions`, che su Debian causava `Permission denied` nel garbage collector → HTTP 500 intermittenti. `handler_id` impostato esplicitamente su `session.handler.native_file` (con `handler_id: ~` Symfony azzerava `save_path`).

### Deploy
- Rimosso `ckeditor:install` dalle `auto-scripts` di `composer.json` e dal runbook ([`deploy/DEPLOY_PREPROD.md`](deploy/DEPLOY_PREPROD.md)); TinyMCE è versionato e non richiede install.

## [5.2.0] - 2026-06-05

Release incentrata sulla **migrazione del framework da Symfony 4 a Symfony 5.4 LTS** (con messa in sicurezza preliminare del codice), più ottimizzazioni di gallery/dashboard, il report email di non conformità e gli strumenti di deploy in pre-produzione.

> ⚠️ **Deploy**: richiede **PHP 8.1+** e il riallineamento dei container; pulire la cache SF4 prima del warmup. Procedura completa in [`deploy/DEPLOY_PREPROD.md`](deploy/DEPLOY_PREPROD.md).

### Cambiato — Migrazione a Symfony 5.4 LTS
- Tutti i componenti `symfony/*` allineati a 5.4 (pin Flex `extra.symfony.require: 5.4.*`); `composer.lock` rigenerato.
- **PHP 8.1+** richiesto (platform `8.1`).
- Controller: rimossa la classe base deprecata → `AbstractController`; eliminato del tutto `$this->getDoctrine()` (iniezione di `EntityManagerInterface`).
- Sicurezza SF5.3+: `encoders` → `password_hashers`, firewall `lazy`, `IS_AUTHENTICATED_ANONYMOUSLY` → `PUBLIC_ACCESS`; `User` implementa `PasswordAuthenticatedUserInterface`, `getUserIdentifier()`, `__serialize`/`__unserialize`.
- Scheduler: `jmose/command-scheduler-bundle` → **`dukecity/command-scheduler-bundle`** (PHP 8 / SF5).
- Event listener portati alle classi SF5 (`RequestEvent`/`ControllerEvent`/`ExceptionEvent`, `isMainRequest()`).
- FOSRest 3: annotazioni `@Rest\{Get,Post,…}` → `@Route(methods=…)`.
- Config migrata (doctrine_migrations, fos_rest, scheduler, dev error routes); `Kernel::configureRoutes` → `RoutingConfigurator`.
- Front controller: `Symfony\Component\Debug\Debug` → `ErrorHandler\Debug`; `loadEnv()` (carica `.env.local`); trusted proxies espliciti.
- Aggiunti i return type nativi richiesti (Kernel, Twig extension, validator, listener, repository).

### Aggiunto
- **Report email di non conformità (congruity)** lato cloud — vedi [`docs/CONGRUITY_EMAIL.md`](docs/CONGRUITY_EMAIL.md).
- **Pacchetto e procedura di deploy in pre-produzione**: `deploy/build-package.sh` (tarball del codice), `deploy/deploy-preprod.sh` (deploy con gate di sicurezza), runbook [`deploy/DEPLOY_PREPROD.md`](deploy/DEPLOY_PREPROD.md).
- Suite di smoke test su DB MariaDB di test (`tests/`, `bin/init-test-db.sh`).

### Migliorato — Performance e UI (gallery + dashboard)
- **Gallery**: thumbnail servite come URL cacheabili con `loading="lazy"` (niente più base64 inline nell'HTML); eliminato l'N+1 sulla sessione (query batch); griglia delle card responsive per risoluzione.
- **Dashboard**: cache a TTL breve (5 min) dei ~25 contatori aggregati (chiave per utente+azienda+filtro); rimosso lo `zoom: 0.65` fisso, sostituito da un **auto-fit** che adatta il contenuto al viewport (no scroll su desktop).

### Risolto
- DBAL 3: `Statement::fetchAll()` (rimosso) → `executeQuery()->fetchAllAssociative()` (155 occorrenze).
- Twig 3: `{% spaceless %}` → `{% apply spaceless %}`; tag sbilanciati nei template email dei report; `path()` con doppio array di parametri.
- `app_getthumb` serviva testo base64 con `Content-Type: image/jpeg` (immagini rotte) → ora invia byte JPEG reali con cache header.
- Scheduler: cablaggio `dukecity`, asset, EntityManager iniettato, rotte del Kernel.
- Dev: la Docker integration della Symfony CLI scavalcava `.env.local` (`DATABASE_URL`) → risolto con la label `com.symfony.server.service-ignore`.

### Sicurezza
- SQL injection: query parametrizzate / cast a `int` in `SearchController`, `ConfiguratoreController`, `CheckController`, `Counters`.
- Segreti rimossi dai file `.env*` versionati (spostati in `.env.local`); chiave AES non più loggata; nonce OAuth casuale; verifica SSL Okta configurabile (`OKTA_SSL_VERIFY`).
- Rimosso l'accesso diretto a `$_GET`/`$_ENV` (superglobals) in controller/comandi/listener, a favore della Dependency Injection.

### Rimosso
- `SensioFrameworkExtraBundle` (`@Method`, `@Security`), `jmose/command-scheduler-bundle`, `koolreport/*`, `roave/better-reflection`.
- Dump SQL (~1,95 GB) e archivi binari purgati dalla **cronologia git** (`.git` 1,1 GB → 536 MB; nessun file > 100 MB), per consentire l'upload su servizi come GitHub.

## [5.1.0] - 2026-05-25

### Aggiunto — Modulo Magazzino (nuovo, fasi 0-3.5 + 1.5)

Modulo completo di gestione magazzino isolato sotto `src/Magazzino/*` con prefisso tabelle `mag_` e route `/magazzino`, progettato per essere scorporabile in futuro come Symfony Bundle. Documentazione completa in [`docs/MAGAZZINO.md`](docs/MAGAZZINO.md).

#### Fase 0 — Scaffolding
- Pacchetti `gedmo/doctrine-extensions` ^3.13 + `stof/doctrine-extensions-bundle` ^1.7
- Pacchetto `sortablejs` ^1.15 per drag&drop (sostituisce jQuery UI deprecato)
- Mapping Doctrine separato `App\Magazzino\Entity` con **attributi PHP 8** (no annotations)
- 3 nuovi ruoli con gerarchia: `ROLE_MAGAZZINO_OPERATOR` → `_MANAGER` → `_ADMIN`
- `MagazzinoVoter` granulare con attributi `magazzino.section.<key>` e `magazzino.entity.<name>.<op>`
- Config `c4rgo_magazzino.yaml` per **toggle sezioni e campi** (modularità a livello fine)
- Layout Twig `magazzino/_layout.html.twig`, webpack entry `magazzino` (1.11 MiB)

#### Fase 1 — Anagrafica articoli
- 9 entity: `Categoria` (Gedmo Tree nested set N-livelli), `Articolo` (~30 campi, UUID v7), `UnitaMisura`, `ArticoloUM` (confezioni con fattore di conversione), `ArticoloImmagine`, `ArticoloAllegato`, `Attributo`, `ArticoloAttributo`, `UserLayoutPref`
- Trait condivisi: `AuditTrait` (timestamp+blameable+softdelete via Gedmo), `AziendaTrait` (multi-tenancy)
- Enum: `TipoArticolo` (materia_prima/semilavorato/prodotto_finito/servizio/kit/variante), `TipoUnitaMisura`
- CRUD completi con **drag&drop SortableJS**: sezioni del form articolo, campi dentro le sezioni, colonne nella lista, albero categorie
- `ModuloConfigService` legge config YAML + overlay layout per-utente
- `LayoutController` API per salvare layout JSON per (user, view_key)
- Migration `Version20260521120000`

#### Fase 1.5 — Import/Export CSV
- `CsvImportService` streaming (yield) con auto-detect delimitatore `, ; \t |` e BOM UTF-8
- `ArticoloImportService` upsert su (codice, azienda), ~25 colonne, decimali tolleranti (virgola/punto), booleani flessibili, batch flush 50 righe
- `ArticoloExportService` con stream + BOM UTF-8 + template scaricabile
- `AnagraficaImportService` per UM, Categorie (two-pass per gerarchia), Causali
- UI + comando CLI `magazzino:articoli:import [--dry-run --azienda --delimiter --encoding]`

#### Fase 2 — Magazzini, movimenti, giacenze
- 10 entity: `Magazzino`, `Zona`, `Ubicazione` (bin), `Causale` (tipo+segno+flag impegna/ordina), `Lotto`, `Seriale`, **`Identificatore` polimorfo** (barcode/QR/RFID/NFC), `Movimento`+`MovimentoRiga` (stato bozza/confermato/annullato, numero progressivo per anno), `GiacenzaRollup` (cache per articolo+magazzino+ubicazione+lotto)
- Enum: `TipoMovimento` (Carico/Scarico/Trasferimento/Inventario/Rettifica/Reso), `SegnoMovimento` (+1/-1/0), `StatoMovimento`, `TipoIdentificatore`
- `GiacenzaService` con `conferma()`/`annulla()`/`rebuild()` idempotenti; gestione trasferimenti (doppia scrittura), impegna, ordina
- Eventi `MovimentoConfermatoEvent` / `MovimentoAnnullatoEvent` — hook per integrazioni MQTT/IoT future
- Comando `magazzino:giacenze:rebuild` per ricalcolo totale o per azienda
- API REST: `GET /api/v1/magazzino/{articoli,giacenze,scan}` con risoluzione multi-livello (Identificatore → Articolo.barcode → ArticoloUM.barcode)
- Migration `Version20260521130000`

#### Fase 3 — Tracciabilità avanzata
- Nuovo campo `Articolo.strategiaScarico` (enum `StrategiaScarico`: FIFO/FEFO/LIFO/Manuale, default FIFO)
- `LottoSelectionService` calcola il piano di scarico ordinato per strategia, restituisce `SuggerimentoLotto[]`
- `TracciabilitaService` per: storico movimenti di un lotto/seriale, "where-is" (giacenza attuale per (magazzino, ubicazione)), lotti in scadenza entro N giorni
- CRUD `Seriale` con stati (disponibile/impegnato/venduto/dismesso/guasto)
- Viste web `/tracciabilita`: ricerca lotto/seriale, storico+timeline, scadenze con filtro 7/15/30/60/90/180 gg
- API REST: `GET /api/v1/magazzino/tracciabilita/{lotti/{id}/{storico,where},articoli/{id}/suggerisci-lotti}`
- **Pulsante ✨ "Auto-seleziona lotti"** sul form Movimento: chiama l'API, popola la riga con il lotto suggerito, aggiunge automaticamente nuove righe se serve attingere a più lotti per coprire la quantità
- Migration `Version20260521140000` (solo `ALTER TABLE`)

#### Fase 3.5 — Sync multi-master e Congruity acquisizioni
- Nuova entity [`AwbItem`](src/Magazzino/Entity/AwbItem.php) (tabella `mag_awb_item`) come sorgente "ATTESO" item-level per la verifica di congruità dello scanner 3D, agganciata via FK opzionale a `mag_articolo` (anagrafica canonica come sorgente unica di verità)
- Chiave logica `(awb, obj_code, azienda_id)`; campi locali `l/w/h_package`, `net/gross_weight`, `inbound_volume` come **override opzionali** per packaging speciale o articoli "freelance"
- `lotto_id` / `seriale_id` opzionali per congruità a livello lotto/matricola
- Nuovo trait `UuidV7Trait` + colonna `uuid` (CHAR(36) UNIQUE, default `UUID()`) aggiunta a `mag_unita_misura`, `mag_articolo_um`, `mag_identificatore` — pronta per sync multi-master cloud↔device senza collisioni di PK (`mag_articolo` aveva già il campo dalla Fase 1)
- Enum `StatoAwbItem` con stati workflow: `pending` / `scanned` / `matched` / `mismatched` / `missing` / `extra`
- Modello **additivo**: il flusso legacy `awbimport` continua a funzionare. Quando `c4rgosaveacq.py` trova righe in `mag_awb_item` per un `(AWB, obj_code)` usa quelle; altrimenti fallback su `awbimport`
- Documento operativo completo della pipeline di congruità in [`docs/CONGRUITY.md`](docs/CONGRUITY.md)
- Migration `Version20260522150000` (CREATE TABLE `mag_awb_item` + 4 FK + 3 `ALTER TABLE … ADD uuid`)

### In sospeso — Modulo Magazzino (fasi pendenti)
- **Fase 4 — Acquisti**: `ListinoFornitore`, `OrdineAcquisto`, `RicezioneMerce` (genera movimento automatico)
- **Fase 5 — Vendite**: `ListinoCliente`, `OrdineVendita`, `DDT` (pre-allocazione lotti)
- **Fase 6 — Configurazione UI**: pannello admin per editing visuale di `c4rgo_magazzino.yaml`
- **Fase 7 — Polish**: dashboard KPI, report stampabili, listener MQTT su movimenti, alert scorta minima, export Excel

Vedi [`docs/MAGAZZINO.md`](docs/MAGAZZINO.md) per il dettaglio completo della roadmap.

### Aggiunto — Configurator e Superadmin (pre-esistente)
- **Filtro anni precedenti** — Dropdown "Previous years" nei 4 template di ricerca (`gallery`, `search`, `search-items`, `list`) per filtrare per anno specifico (ultimi 10 anni). Utilizza il formato URL `Y[anno]` già supportato dai controller
- Autenticazione a due fattori (2FA) via email con SchebTwoFactorBundle
- Configurazione `config/packages/scheb_2fa.yaml` e `config/routes/scheb_2fa.yaml`
- Implementazione `TwoFactorInterface` nell'entity `User`
- Favicon `public/favicon-32x32.png`
- **Modulo Superadmin Management** — Area completa di gestione piattaforma sotto `/{locale}/sa/`
  - Dashboard superadmin con contatori e navigazione tile-based
  - CRUD Aziende (con upload logo, controllo dipendenze Luoghi/Utenti)
  - CRUD Luoghi (filtro per azienda, controllo dipendenze Macchine)
  - CRUD Modelli (con upload immagine, gestione associazioni Componenti e Comandi)
  - CRUD Macchine (con editor JSON licenza, gestione Devices installati, log esecuzioni comandi)
  - CRUD Comandi (con editor JSON parametri)
  - CRUD Comandi Exec (log esecuzioni, creazione nuove esecuzioni)
  - CRUD Famiglie Componenti (con upload immagine, associazione Montaggi)
  - CRUD Montaggi
  - CRUD Componenti (gerarchia articoli/varianti, classificazione, supply chain)
- 10 controller SA: `DashboardController`, `AziendeController`, `LuoghiController`, `ModelliController`, `MacchineController`, `ComandiController`, `ComandiExecController`, `FamigliaComponentiController`, `MontaggiController`, `ComponentiController`
- 9 FormType: `AziendeType`, `LuoghiType`, `ModelliType`, `MacchineType`, `ComandiType`, `ComandiExecType`, `FamigliaComponentiType`, `MontaggiType`, `ComponentiType`
- 30 template Twig per l'area SA (layout, dashboard, list/form/detail per ogni entita)
- `AuditFieldsListener` — Listener Doctrine per impostazione automatica campi audit (CreatorId, CreationDate, ModifyId, ModifyDate)
- Migrazione `Version20260220120000` — Tabella `comandi_modelli` (ManyToMany Modelli-Comandi)
- CSS dedicato area SA (`sa_management.css`)
- Relazione ManyToMany Modelli-Comandi (entity `Modelli` e `Comandi`)
- Link dashboard SA dal pannello di gestione (solo per ROLE_SUPERADMIN)

### Modificato
- **Internazionalizzazione form** — Convertiti tutti i testi hardcoded in chiavi di traduzione XLIFF (IT/EN)
  - 13 Form Type SA (`src/Form/Type/`): label, choice, placeholder e pulsanti convertiti a chiavi `form.*`, `choice.*`, `placeholder.*`, `action.*`
  - 3 Form Type legacy (`src/Form/`): ContenitoriType, PalletsType, MacchineType — stessa conversione
  - 4 Controller: DMXLedLightController, CloudCamController, ConfigDeepNestController, ConfiguratoreDOController
  - ~90 nuove trans-unit in `messages.it.xlf` e `messages.en.xlf`
  - Creati `configuratore.it.xlf` e `configengine.it.xlf` (domini dedicati)
  - Aggiornati `configuratore.en.xlf` e `configengine.en.xlf` con le stesse chiavi
  - Corretto typo "Lenght" → "Length" in PalletsType.php
- Migrazione completa da SwiftMailer a Symfony Mailer (22 file modificati)
  - `ReportService` — Iniettato `MailerInterface $reportMailer`, rimosso `Swift_SmtpTransport` manuale, migrato image embedding (`embedFromPath` + `cid:`)
  - `DataController` — Migrato `generateemail()`, `generatedoc()`, `bkgeneratedoc()`, `sendmail()`; `Swift_Attachment` → `$email->attach()`
  - `C4rgoRoiCalcController` — Rimosso uso ibrido Swift+Mailer, consolidato su `MailerInterface`
  - `CommentNotificationSubscriber` — `Swift_Mailer` → `MailerInterface`
  - `InfoMacchinaCommand` — Iniettato `MailerInterface $reportMailer`, rimosso transport manuale
  - `ListUsersCommand` — `Swift_Mailer` → `MailerInterface`
  - 11 comandi report (RepDay, RepWeek, RepMonth, ecc.) — Rimosso `Swift_Mailer`
  - 3 comandi CED (SendCED, MoreSendCED, ReSendCED) — Rimosso `Swift_Mailer`
- `SecurityController` — Supporto flusso Okta OAuth2 con redirect condizionale
- `User` entity — Aggiunto campo `authCode` per 2FA, implementazione interfaccia `TwoFactorInterface`
- `OktaApiService` — Aggiornata logica di autorizzazione
- Template `welcome/index.html.twig` e `admin/user_management.html.twig` — Aggiornamenti interfaccia
- `ImageGen` — Correzioni generazione immagini
- `Entity/Modelli` — Aggiunta relazione ManyToMany `$Comandi` con JoinTable `comandi_modelli`
- `Entity/Comandi` — Aggiunta relazione ManyToMany inversa `$Modelli`, rimossa relazione inversa `$Macchine`
- `Entity/Macchine` — Rimossa relazione ManyToMany `$Comandi` diretta, `getComandi()` ora delega al Modello
- `config/services.yaml` — Parametri upload directory, registrazione `AuditFieldsListener`
- `templates/admin/management_pannel.html.twig` — Aggiunto link alla dashboard SA (condizionale ROLE_SUPERADMIN)
- `templates/admin/company_management.html.twig` — Ristrutturata da 1 tab a 4 tab (Company, Settings, Locations, Machines)
- `CompanyManagementController` — Aggiunto caricamento Luoghi e Macchine dell'azienda

### Rimosso
- `symfony/swiftmailer-bundle` da `composer.json`
- `SwiftmailerBundle` da `config/bundles.php`
- File di configurazione `config/packages/swiftmailer.yaml`, `config/packages/dev/swiftmailer.yaml`, `config/packages/test/swiftmailer.yaml`
- Relazione ManyToMany diretta Macchine-Comandi (sostituita da Modelli-Comandi)

### Corretto
- **Sessioni DB** — Disabilitato PdoSessionHandler (sessioni su file) per evitare errore "mysqli scheme not supported" e tabella `sessions` mancante. Commentato servizio in `services.yaml` con TODO per riattivazione futura
- **Collation mismatch Configuratore** — Aggiunto `COLLATE utf8mb4_unicode_ci` esplicito nei JOIN delle query SQL tra tabelle temporanee e tabelle permanenti nei controller ConfiguratoreMO, ConfiguratoreSO e ConfiguratoreDO. Fix cross-compatibile MySQL 8.0 / MariaDB
- **Mixed content HTTPS** — Attivato `Request::setTrustedProxies` con `HEADER_X_FORWARDED_ALL` in `index.php` per corretta generazione URL HTTPS dietro reverse proxy nginx

### In corso
- Modulo configuratore 3D contenitori

---

## [5.0.1] - 2026-01-30

### Corretto
- Bug nell'invio email dopo migrazione a Symfony Mailer
- Problema generazione PDF (aggiornamento DomPDF a v3.1+)
- Problema grafico nell'interfaccia
- Errori nel form file manager
- Errori di configurazione YAML (voci duplicate, conflitti, indentazione)
- Timeout nelle chiamate AJAX
- URL servizi phone
- Errore di classe non trovata (namespace corretto)
- Errore blocco duplicato in un template
- Identificativo dispositivo MQTT

### Aggiunto
- Componente file manager per file e immagini (sostituzione di artgris)
- Componente image manager
- Tab SMS nell'interfaccia
- Moduli GPS e mobile nei template
- Avvio automatico container Docker all'avvio del server

### Modificato
- Migrazione da SwiftMailer a Symfony Mailer (parziale)
- Sostituzione componente upload nella form contenitori
- Pagine configurazione dispositivi spostate in area superadmin
- Rimosso prefisso locale (`/{_locale}`) dalle rotte API (`/api/v1`)
- Sostituito accesso diretto a Mosquitto con microservice Python
- Rimosso image manager controller legacy

---

## [5.0.0] - 2025-12-09

Aggiornamento maggiore: migrazione da PHP 7.1 a PHP 7.4 e aggiornamento dipendenze Symfony 4.x.

### Modificato
- Requisito PHP minimo portato da 7.1 a 7.4
- Aggiornato `ManagerRegistry` al nuovo namespace (`Doctrine\Persistence`)
- Aggiornato php-cs-fixer da v2.19 a v3.4
- Sostituito `twig/extensions` (deprecato) con `twig/intl-extra` + `LegacyIntlExtension`
- Aggiornato `phpunit.xml.dist` per PHPUnit 9.5
- Sostituiti metodi deprecati in vari servizi
- Aggiornate librerie (`composer update`)

### Rimosso
- `sensiolabs/security-checker` (deprecato, endpoint offline)
- `symfony/web-server-bundle` (deprecato)
- Configurazione DAMA DoctrineTestBundle

### Aggiunto
- Supporto MQTT per dispositivi SIM7600 (campo `mqttDeviceId`)
- Supporto mobile e GPS con MQTT
- Supporto ACpu (Application CPU)
- `SIM7600Service` per comunicazione IoT
- `Sim7600MqttListenerCommand` per ascolto MQTT
- Three.js per visualizzazione 3D

---

## [4.x] - 2021-10 / 2025-11

### Funzionalita principali sviluppate in questo periodo

#### Gestione Cargo e Contenitori
- CRUD completo per pallets e contenitori
- Configuratore 3D per disposizione oggetti nei contenitori
- Barre percentuali riempimento
- Gallery con carousel
- Form supporti con autocomplete per item code

#### Autenticazione e Sicurezza
- Integrazione Okta OAuth2 (SSO)
- JWT per API REST (Lexik JWT Authentication)
- Logging accessi con geolocalizzazione IP
- Sistema ruoli gerarchico (User, Admin, SuperAdmin, ScanAdmin)
- Crittografia dati sensibili (AES-256-CBC)

#### Reportistica
- Generazione report in CSV, Excel, PDF
- Doppio trasporto email (transazionale + report)
- Report pianificati con CommandSchedulerBundle

#### API REST
- Endpoint registrazione dispositivi
- API barcode scanning
- WebSocket per comunicazione real-time
- Integrazione Oracle (Baker Hughes)

#### Gestione Utenti e Aziende
- Pannello admin gestione utenti con avatar
- Gestione aziende multi-tenant
- Sistema gruppi con ereditarieta ruoli
- Profili utente con dati anagrafici

#### Internazionalizzazione
- Supporto 18 lingue
- Prefisso locale nelle rotte
- Traduzioni XLIFF

#### Infrastruttura
- Containerizzazione con Docker e Docker Compose
- CI con Travis CI
- Geocoding con BazingaGeocoderBundle
- Generazione QR code e barcode

---

## [1.0.0] - 2021-10-20

### Aggiunto
- Primo commit del progetto
- Struttura base Symfony 4
- File SQL per creazione database
- Configurazione Apache in README
- File `.env` base
