Revisione blocchi 2 e 3: file protetti, ripristino atomico, import sicuri

Blocco 2
- Esiti delle azioni Livewire come avvisi (trait Avvisa) invece di flash
  che comparivano alla pagina dopo.
- PDF, miniature e versioni dei territori non più serviti da nginx su
  /storage: TerritorioFileController con login, permessi e cache privata.
- Ripristino tutto o niente: verifica dell'archivio, una transazione con
  import, piantine, blocco account e scambio dei file annullabile.
- Backup formato 2 con piantine, storico PDF e area KML.
- Posto unico preso in un passo (Cache::lock) e rispettato anche da
  import XML e PDF.
- Import PDF da cartella: abbinamento corretto (1-A, 12 B, ambigui),
  caricamento a lotti, file non PDF ignorati, PDF sostituito nello storico.
- L'import XML elimina i file dei territori che non usa più; comando
  territori:file-orfani per quelli accumulati prima.

Blocco 3
- Password attuale richiesta al cambio volontario; AuthenticateSession
  chiude le sessioni aperte altrove.
- Niente nomi dei proclamatori in chiaro nel registro attività.
- Export XML senza remember_token; chi riceve l'hash dal file deve
  cambiare password.
- Campagne: ultimo giorno incluso, percentuale sui territori distinti.
- Coda: retry_after 960 s, stop_grace_period 16m, Redis volatile-lru,
  impostazioni rilette a ogni job.
- Azzeramento che cancella anche piantine, area KML e anteprime.
- Pulsanti profilo e menu che non si nascondevano; icona del sito.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 15:01:41 +02:00
parent 056d95cfcb
commit 426a95211d
78 changed files with 3971 additions and 429 deletions

View File

@@ -77,69 +77,60 @@ e l'unica che richiede una decisione, non del lavoro.
## Lavori in corso — 12 ottobre 2026 (riprendere da qui)
### Già fatto e committato su `dev` (commit `d433c93`, NON ancora pushato)
Commit su `dev`, **nessuno pubblicato** (il push fa partire il deploy sulla demo):
- `d433c93` — Modulo S-13 sul modulo ufficiale (regole in
`app/Services/Registro/ModuloS13.php`) e blocco 1 della revisione
(integrità dati).
- `056d95c` — Editor piantine: bozza automatica, un solo "Pubblica" con
anteprima, stati Bozza / Pubblicata / Modifiche da pubblicare. Come funziona:
[Le piantine sono una sotto-app](#le-piantine-sono-una-sotto-app-renderer-esterno-agganci-minimi).
- Il commit successivo chiude i blocchi 2 e 3 della revisione (sotto).
- **Modulo S-13** riscritto su modulo ufficiale (FPDI), regole decise con l'utente:
20 righe/foglio, tutti i territori attivi, ciclo da 4 con riporto, riporto
iniziale = ultimo rientro prima dell'anno, assegnazioni a cavallo d'anno su
entrambi i fogli (sul vecchio senza rientro), nomi "Cognome I.", scelta per
data. Codice in `app/Services/Registro/ModuloS13.php`.
- **Blocco 1 della revisione** (integrità dati): date export/import che
slittavano di un giorno, import XML che accettava file estranei, caratteri di
controllo, territori senza zona scartati, admin estromesso dall'import,
doppia assegnazione (`Assegnazione::sovrappostaA()` + lock), ultimo admin,
`APP_KEY` sovrascritta dall'entrypoint (`APP_KEY_ROTATE=1`). Vedi la tabella
"Bug corretti". Suite: 328 test verdi.
- Il push su `dev` fa partire il deploy su demo: va fatto quando si decide.
Produzione: al 12/10/2026 non esiste ancora (il sito gira in Docker sul PC di
sviluppo, più la demo da `dev`). Nessuna correzione una tantum da fare.
### Finito, NON ancora committato: riprogettazione dell'Editor Piantine
### Revisione profonda: blocchi 2 e 3 chiusi
Decisioni dell'utente: due pagine separate (gestione / editor); **un solo
pulsante "Pubblica"** (immagine + PDF) con **anteprima obbligatoria** da
confermare, disponibile da editor e gestione; dall'editor **si modificano i
dati del territorio** (salvati subito nel gestionale); uso soprattutto da PC ma
anche **tablet**; **stato di bozza**: in bozza va solo il disegno, salvata
**automaticamente**; stati Bozza / Pubblicata / Modifiche da pubblicare.
Come funziona: sezione [Le piantine sono una sotto-app](#le-piantine-sono-una-sotto-app-renderer-esterno-agganci-minimi).
| Problema | Correzione |
|---|---|
| Esiti delle azioni Livewire persi o mostrati alla pagina dopo (11 componenti) | trait `Avvisa` + `<x-avvisi>`; un test vieta `session()->flash()` senza redirect |
| PDF, miniature e versioni scaricabili da `/storage/territori-*` senza login (nomi indovinabili, cache Cloudflare 7 giorni) | nginx risponde 404 su `/storage/territori-`; `TerritorioFileController` con permessi della pagina e cache privata; `Territorio::pdfUrl()` / `miniaturaUrl()` |
| Ripristino non atomico | estrazione verificata (CRC, byte) → una transazione con import, piantine, blocco account e scambio file (`rename`, annullabile) → file vecchi cancellati solo dopo il commit |
| Piantine, storico PDF, area KML fuori dal backup | formato archivio 2 (`piantine.json`, `piantine/…`, `PiantineBackup`); gli archivi di formato 1 si ripristinano lo stesso |
| Blocco "una sola operazione" ignorato da import XML/PDF, e controllo+presa in due passi | `BackupOperationState::occupa()` sotto `Cache::lock`, usato da tutte le porte; rilascio anche su errore e in `failed()` |
| Import PDF da cartella: `1-A` → territorio 1, `12 B` ambiguo, solo 20 file, `Thumbs.db` blocca tutto, `__MACOSX` | abbinamento "cifre + al massimo una lettera"; ambigui scartati; caricamento a lotti dal browser; file non PDF ignorati |
| Import PDF e caricamento dalla scheda cancellavano il vecchio PDF prima di scrivere il nuovo | prima il nuovo, poi il vecchio nello storico (`PiantinaPdfService::archiviaCorrente`, origini `import` / `manuale`) |
| L'import XML lasciava sul disco i file dei territori ricreati (nel PC: 549 file, 80 MB) | l'import elimina i file non più usati; per i vecchi c'è `php artisan territori:file-orfani` (elenca; `--elimina` cancella) — **da lanciare quando si decide** |
| Azzeramento lasciava piantine orfane, area KML, anteprime | cancellate anche quelle |
| `/cambia-password` senza password attuale | richiesta, tranne dopo un ripristino (cambio obbligatorio) |
| Sessioni aperte altrove non chiuse al cambio password | `AuthenticateSession` nel gruppo web; il ripristino conserva l'hash di chi lo esegue, così non viene buttato fuori |
| Nomi dei proclamatori in chiaro nel registro attività | si salva `proclamatore_id`, il registro mostra il nome attuale; migrazione `2026_10_12_000010` ripulisce le voci vecchie |
| Export XML con `remember_token`; l'import XML non obbligava a cambiare password | token non esportato e azzerato all'import; chi riceve l'hash dal file deve cambiare password |
| Campagne: sparivano l'ultimo giorno; percentuale oltre il 100% | confronto per data; si contano i territori attivi distinti |
| `retry_after` 90 s con job fino a 900 s | `config/queue.php`: 960 s |
| Worker ucciso dopo 10 s al riavvio | `stop_grace_period: 16m` |
| Redis `allkeys-lru` poteva sfrattare i job in coda | `volatile-lru` |
| Cache statica di `Setting` vecchia nel worker | svuotata a ogni job (`Queue::before`) |
| Pulsanti profilo/menu che non si nascondevano (barra che trabocca su telefono) | `.tm-btn` sta fuori dai layer di Tailwind e vince su `hidden`: si nasconde il contenitore |
Stato al 12/10/2026:
- Codice completo: modello, servizio di pubblicazione + job, finestra condivisa,
editor e gestione riscritti, JS e CSS. Migrazione
`2026_10_09_000010_bozza_e_pubblicazione_piantine`.
- Test: `PiantineEditorTest` (22), `PiantineManagerTest`,
`PubblicazionePiantinaTest` (nuovo). Suite intera: **360 test verdi**.
- Provato in browser (Playwright + Edge, in locale) su PC 1440, iPad
orizzontale e verticale, iPhone: disegno, modifica vertici (8 maniglie su
poligono), collegamento, anteprima generata dal renderer vero, conferma,
stato "Pubblicata", filtri della gestione. Corretti lì: tendina territori che
segnava tutti "(disattivato)" (colonna `attivo` non caricata), zoom sopra la
ricerca dell'elenco, barra strumenti sopra il pannello su tablet e fuori
schermo su telefono, crediti mappa stampati sul foglio del pannello.
- Aggiunto `.gitattributes` (`*.sh text eol=lf`): con `core.autocrlf=true`
l'entrypoint usciva con CRLF e il container locale non partiva ("no such
file or directory"). Nel repository era già LF: il deploy non era a rischio.
- **Non provato:** modifica vertici al tocco su un tablet vero, MultiPolygon
in modifica vertici (si modifica il primo poligono).
- Visto e **non** corretto (fuori tema): su telefono la barra superiore
dell'app trabocca con nome/email lunghi ("Esci" fuori schermo);
`/favicon.ico` risponde 404.
Non cambiato, di proposito: `trustProxies('*')` (commento in
`bootstrap/app.php`: con `real_ip` di nginx, restringerlo romperebbe HTTPS).
### Revisione profonda: blocchi ancora aperti
Suite: **427 test verdi**. Provati in browser in locale: file protetti (404
senza login, PDF e miniature con login), caricamento a lotti (25 PDF in 2
lotti, `Thumbs.db` ignorato, nessun territorio toccato, posto liberato).
- **Blocco 2:** avvisi in tutte le ~10 pagine che oggi li perdono (usare il
trait `Avvisa`); PDF dei territori serviti da nginx su `/storage` senza
autenticazione (spostare su disco privato); ripristino non atomico e blocco
"una sola operazione" ignorato da import XML/PDF; piantine e storico PDF
assenti dal backup; import PDF da cartella (`1-A` sovrascrive `1`, solo 20
file per `max_file_uploads`, `Thumbs.db` blocca tutto, `12 B` ambiguo).
- **Blocco 3:** `/cambia-password` senza password attuale, sessioni non
invalidate, `trustProxies('*')`, nomi in chiaro nell'activity log, export XML
con hash/token, campagne (ultimo giorno, percentuali), `retry_after` 90 s,
`stop_grace_period` del worker, Redis `allkeys-lru`, cache statica di
`Setting` nel worker, azzeramento che lascia piantine e KML.
- Produzione: al 12/10/2026 non esiste ancora (il sito gira in Docker sul PC
di sviluppo, più la demo da `dev`). Nessuna correzione una tantum delle date
slittate da fare.
**Non provato / aperto:**
- chiusura delle sessioni con due browser veri; coda e worker con un riavvio
reale; modifica vertici al tocco su tablet vero;
- dopo un ripristino le piantine pubblicate risultano "Modifiche da pubblicare"
(l'import ricrea i territori con id nuovi): verso sicuro, basta ripubblicare;
- `corrente()` / `liberaCorrente()` di `BackupOperationState` fanno get+forget
senza mutex (finestra di millisecondi);
- l'upload Livewire ha `throttle:60,1`: oltre ~1200 file al minuto qualche
lotto riceve 429 (il browser lo segnala);
- numeri di territorio con lettere davanti ("A12") non si abbinano da nome
file: il PDF si carica dalla scheda.
---
@@ -748,13 +739,16 @@ Scelte non ovvie:
cancellazioni scrivono righe nuove — e subito dopo si scrive l'unica riga che
sopravvive, quella dell'azzeramento stesso.
- **Ordine del restore: staging → database → file.** I file si estraggono
prima in `storage/app/backup-restore/<uuid>`, così un archivio corrotto o
fuori limite fallisce mentre i dati correnti sono ancora intatti; poi
`importXmlFromContent()` in transazione; poi la sostituzione dei file. Il
disco `public` viene **svuotato**, non fuso: i percorsi vivono nelle righe
appena riscritte, e tenere i vecchi file lascerebbe PDF orfani che nessuna
schermata mostra e nessuna cancellazione raggiunge.
- **Ordine del restore: verifica → una transazione → pulizia.** Dal 12/10/2026
(`RestoreBackupArchive`): si estrae e si verifica tutto (CRC, byte, il
`piantine.json` del formato 2) prima di toccare qualcosa; poi una sola
`DB::transaction` contiene import XML, tabelle delle piantine, blocco degli
account e, per ultimo, lo scambio dei file (`BackupArchive::scambiaFile()`:
solo `rename()` sullo stesso volume, annotati e annullabili). Se qualcosa
fallisce si torna allo stato di prima; i file vecchi aspettano in
`storage/app/backup-precedenti/<uuid>` e si cancellano solo dopo il commit.
Il disco `public` viene sostituito, non fuso: i percorsi vivono nelle righe
appena riscritte.
- **Limiti sull'archivio in ingresso.** Un `.zip` è compresso: senza tetti su
numero di entry, dimensione della singola entry e totale decompresso, pochi
@@ -1245,21 +1239,12 @@ pubblicate da Laravel sono state rimosse perche' i paginatori sono tutti dentro
componenti Livewire, che risolvono la vista dal namespace `livewire::` e non da
`pagination::`. L'esclusione resta a guardia di future viste di terze parti.
### 7. Il backup non contiene l'area KML delle piantine
### 7. Area KML, piantine e storico nel backup — chiuso il 12/10/2026
`BackupArchive` impacchetta il solo disco `public` (`storage/app/public`), e
`PiantineArea` scrive in `storage/app/piantine/` — una cartella **sorella**, non
dentro. Verificato: dopo un ripristino su volumi vuoti l'area va ricaricata a
mano dal KML originale, che quindi conviene conservare fuori di qui.
I disegni invece si salvano: stanno in `territorio_mappe`, e da quando la chiave
esterna è `nullOnDelete` sopravvivono anche allo svuotamento di `territori` che
fa ogni import XML — scollegati, ma recuperabili.
Sistemarlo vuol dire estendere `BackupArchive` a una seconda cartella, cosa che
tocca anche il formato dell'archivio (`FORMAT_VERSION`) e il ripristino. Non è
stato fatto perché il KML è un file che l'utente ha già sul suo computer: la
perdita è un fastidio, non un dato irrecuperabile.
Dal formato 2 l'archivio contiene `piantine.json` (righe di `territorio_mappe`
e `territorio_pdf_versioni`, con il numero del territorio per ricollegarle) e
la cartella `piantine/` dell'area KML. Un archivio di formato 1 si ripristina
lo stesso: piantine e storico restano vuoti, l'area in uso non si tocca.
---