Piantine staccate dai territori, editor a schermo intero, restyle

Le piantine non dipendono piu' da un territorio esistente: territorio_id e'
nullable, la chiave esterna e' nullOnDelete invece di cascadeOnDelete, e `nome`
fa da etichetta finche' il collegamento manca. Si disegna guardando le strade e
si decide dopo di chi e' il disegno; se il territorio non c'e', lo si crea
dall'editor con numero, zona, tipologia e confini — zona e tipologia comprese,
create sul posto quando mancano.

Il cambio di cascata chiude anche una perdita di dati silenziosa: ogni import
XML svuota `territori` in blocco e finora si portava via tutte le piantine.

Il disegno passa su /piantine/editor, a schermo intero con telaio proprio: su
una mappa larga un terzo di schermo un confine si sbaglia, e finisce stampato.
/piantine resta la gestione — area, generazione, collegamenti, storico.

Cartolina S-12 reimpaginata: pagina 1 il modulo con la Localita' (che ora e' il
nome della zona, non un campo duplicato) e i confini scritti nello spazio
centrale, con il corpo del testo che si adatta alla lunghezza; pagina 2 la
piantina sola al 98%. Aggiunta l'anteprima, che compone il PDF al volo senza
toccare pdf_path ne' lo storico.

Immagine: JPEG invece di PNG (0,9 MB contro 5 per territorio), composta alle
tessere dello zoom piu' alto e ridotta con Lanczos, nelle proporzioni della
pagina cosi' riempie il foglio invece di lasciare fasce bianche. Il tratto del
confine e' espresso in pixel dell'immagine finale: si vede il confine e si legge
la via che ci passa sotto. L'attribuzione OSM si dimensiona con l'immagine,
come la ODbL richiede.

Rimossa la rigenerazione massiva: job, stato in cache e interfaccia.

Sistema visivo ripassato su tutta l'applicazione: le scale gray e indigo di
Tailwind sono ridefinite nei token, cosi' le 37 viste si riallineano insieme
invece di essere riscritte una per una. Grigi caldi, un accento solo con quattro
significati, carattere servito dall'applicazione e non da Google Fonts, via il
gradiente della testata. Aggiunto il link "salta al contenuto", che mancava.

Corretto un guasto d'ambiente che bloccava TUTTI gli upload in sviluppo: dalla
1.26 il fastcgi_params di nginx passa HTTP_HOST come $host, che perde la porta,
e ogni URL firmato smetteva di validare — l'endpoint di upload di Livewire
rispondeva 401.

294 test verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-28 16:41:22 +02:00
parent 7e4c8aa9b8
commit 9c37c1ad7b
30 changed files with 3032 additions and 982 deletions

View File

@@ -4,7 +4,7 @@ Stato del progetto per chi ci mette mano dopo. Per installazione, deploy e uso
quotidiano vedi [README.md](README.md); qui c'è il contesto che il README non
può dare: perché le cose sono come sono, e cosa resta aperto.
Ultimo aggiornamento: **31 luglio 2026**.
Ultimo aggiornamento: **28 agosto 2026**.
---
@@ -22,7 +22,7 @@ redeploy, login, tutte le pagine, code e scheduler.
| Sviluppo locale | ✅ stesso compose, profilo `dev` |
| Backup / ripristino / azzeramento | ✅ verificati end-to-end in locale |
| Ripristino da browser in linea | ⚠️ **limitato a 100 MB da Cloudflare** — vedi [Cosa resta aperto](#cosa-resta-aperto) |
| Test automatici | ⚠️ **impalcatura pronta, 57 test** — copertura ancora parziale |
| Test automatici | ⚠️ **impalcatura pronta, 294 test** — copertura ancora parziale |
**Punto in cui ci si è fermati (30/07/2026).** Su `develop` è pubblicato tutto:
backup completo `.zip`, ripristino, azzeramento, cambio password forzato, barre
@@ -39,6 +39,18 @@ architetturali](#le-piantine-sono-una-sotto-app-renderer-esterno-agganci-minimi)
Al primo deploy il compose costruisce anche l'immagine del renderer; non
servono variabili nuove in Dokploy.
**Il 28/08 la sotto-app è stata rifatta e l'interfaccia ripassata.** Le piantine
si sono staccate dai territori (una piantina vive anche scollegata, il territorio
si crea dall'editor), il disegno è passato su una pagina a schermo intero, la
cartolina S-12 è stata reimpaginata (confini sulla prima pagina, piantina al 98%
sulla seconda) e la rigenerazione massiva è stata rimossa. In più il sistema
visivo è stato ripassato su tutta l'applicazione, non solo qui: vedi [Il sistema
visivo sta nei token](#il-sistema-visivo-sta-nei-token-non-nelle-viste).
Nello stesso giro è emerso e corretto un guasto d'ambiente che bloccava **tutti**
gli upload in sviluppo: vedi [`HTTP_HOST` a nginx deve conservare la
porta](#http_host-a-nginx-deve-conservare-la-porta).
Il 30/07 sono state fatte due cose.
**La suite di test, che prima non esisteva**: 79 test eseguibili con un comando,
@@ -166,13 +178,13 @@ password `secret`.
### Le piantine sono una sotto-app: renderer esterno, agganci minimi
La pagina **Piantine** (`/piantine`, solo amministratore) genera le mappe dei
territori: si carica il KML dell'area della città, si disegnano i poligoni su
OpenStreetMap (Leaflet), e da ogni poligono si ottiene un'immagine ad alta
definizione e — a scelta — il PDF sul modulo ufficiale S-12
(`resources/pdf/S-12_I.pdf`, importato con FPDI) che sostituisce quello del
Le **Piantine** (solo amministratore) generano le mappe dei territori. Si carica
il KML dell'area della città, si disegnano i confini su OpenStreetMap (Leaflet)
nell'editor a schermo intero, e da ogni disegno si ottiene un'immagine ad alta
definizione e — a scelta — la cartolina sul modulo ufficiale S-12
(`resources/pdf/S-12_I.pdf`, importato con FPDI) che sostituisce il PDF del
territorio. Il PDF precedente finisce nella tabella `territorio_pdf_versioni`
ed è ripristinabile dalla stessa pagina.
ed è ripristinabile dalla pagina di gestione.
Il rendering vero e proprio **non sta nell'applicazione**: lo fa il container
`map-renderer` (`docker/renderer/`, Node + sharp), che scarica le tessere OSM
@@ -192,9 +204,104 @@ Le geometrie vivono nella tabella separata `territorio_mappe`, l'area KML in
`storage/app/piantine/`: rimuovere la sotto-app significa togliere questi
pezzi, non disfare il gestionale.
La rigenerazione massiva (`Jobs/RigeneraPiantine`) usa lo stesso schema di
avanzamento di backup e import PDF (`PiantineOperationState`, cache + poll
della pagina, un'operazione alla volta).
**Il disegno viene prima del numero.** Una piantina non ha bisogno di un
territorio: `territorio_mappe.territorio_id` è nullable e il collegamento si fa
(e si disfa) quando si vuole, con `nome` come etichetta finché manca. Si taglia
l'area guardando le strade e solo dopo si decide di chi è — e se quel territorio
non esiste, lo si crea dall'editor col solo numero. La chiave esterna è
`nullOnDelete`, non più `cascadeOnDelete`: cancellare un territorio, o svuotare
`territori` come fa ogni import XML, non porta più via i disegni.
**Due pagine, non una.** `/piantine` tiene area, generazione, collegamenti e
storico; `/piantine/editor` è l'editor a schermo intero, con un telaio suo
(`layouts/editor`) senza barra laterale. Tagliare un confine su una mappa larga
un terzo di schermo vuol dire sbagliarlo, e quel disegno finisce stampato.
La rigenerazione massiva è stata tolta: job, stato in cache e interfaccia. Si
rigenera un territorio alla volta dall'elenco.
**La cartolina e la piantina sono due pagine distinte.** Il riquadro dell'S-12 è
largo sette centimetri: una mappa di quartiere lì dentro non si legge. Quindi
pagina 1 è il modulo con Località, numero e i **confini scritti** nello spazio
centrale (il corpo del testo si restringe da solo finché ci sta), pagina 2 è la
piantina sola al 98% della pagina.
**La "Località" del modulo è il nome della zona.** Era un campo globale, poi un
campo della piantina, e in entrambi i casi era un doppione: la zona esiste già,
dice la stessa cosa («Palermo Montepellegrino») ed è quella che si aggiorna
quando la congregazione riorganizza il lavoro. Due posti dove scrivere lo stesso
nome divergono prima o poi, e sulla carta finisce quello sbagliato. L'elenco
marca i territori collegati senza zona: senza, l'intestazione della cartolina
esce vuota e ce se ne accorge in stampa.
**L'anteprima non tocca niente.** `piantine.anteprima` compone il PDF al volo e
lo serve `inline`: non aggiorna `pdf_path`, non archivia versioni, non lascia
file. «Genera PDF» manda il documento in vigore, e scoprire un confine sbagliato
dopo significa trovarselo già nello storico.
**L'immagine è JPEG, sovracampionata e nella forma della pagina.** Tre scelte
che vanno lette insieme (`config/piantine.php`, `docker/renderer/server.js`):
- Il renderer compone alle tessere dello zoom più alto che entri in
`max_side × 2` e poi riduce con Lanczos. Le etichette OSM sono disegnate
dentro la tessera a dimensione fissa: salire di uno zoom le rende più
definite, e ridurre dopo porta quella nitidezza dentro l'immagine finale.
- Chiede al renderer le **proporzioni della pagina** (`aspetto`): un territorio
quasi quadrato su una cartolina orizzontale, tenendo le sue proporzioni,
lascerebbe due fasce bianche larghe un quinto di foglio. Allargando il
ritaglio si mostrano più dintorni invece di carta vuota.
- JPEG a qualità 86 invece di PNG: 0,9 MB invece di 5 per territorio. Con 274
territori è la differenza fra qualche centinaio di MB e diversi GB.
L'attribuzione OSM si dimensiona sull'immagine finale, non su una misura fissa:
22 px su 3400 sarebbero meno di un millimetro stampato, cioè presenti nel file
ma invisibili a chi tiene la cartolina — e la ODbL la vuole leggibile. Per lo
stesso motivo il tratto del confine è espresso in pixel dell'immagine finale e
riportato alla scala di composizione: deve dire dove passa il confine senza
cancellare la via che ci corre sotto, che è proprio quella che serve leggere.
### Il sistema visivo sta nei token, non nelle viste
`resources/css/app.css` **ridefinisce le scale `gray` e `indigo` di Tailwind**
dentro `@theme`. È la cosa che sorprende di più aprendo il file, ed è
deliberata: le trentasette viste sono scritte con le utility (`text-gray-600`,
`bg-indigo-50`), e cambiare la scala le riallinea tutte insieme. L'alternativa
— riscrivere le classi vista per vista — a ogni file dimenticato lascia in giro
il grigio di prima accanto a quello nuovo, e nessuno se ne accorge finché non
apre proprio quella pagina.
Da qui tre conseguenze che vanno sapute prima di metterci mano:
- **I grigi sono caldi.** Quelli di serie di Tailwind tirano al blu e, sommati a
un accento indaco, davano all'interfaccia una dominante fredda. Questi elenchi
diventano moduli di carta che passano di mano: un fondo caldo li avvicina a
quello che sono. Non mescolare grigi freddi qui dentro.
- **L'accento è uno.** Indaco = azione, verde = fatto, ambra = in sospeso,
rosso = distrugge dati. Se una cosa non rientra in queste quattro, è grigia.
Prima le tinte erano cinque e decorative.
- **Il carattere è servito dall'applicazione** (`@fontsource-variable/figtree`,
dipendenza npm), non da Google Fonts. Un gestionale che tiene nomi e indirizzi
di persone non deve chiamare un dominio di terze parti a ogni pagina caricata,
e così funziona anche senza rete.
I componenti `tm-*` (pulsanti, console, card, campi, elenchi, editor) stanno
tutti qui e **fuori da `@layer`**, quindi vincono sulle utility: un pulsante non
si può ritoccare a metà con le classi. Chi aggiunge una schermata usa questi,
non catene di utility nuove.
### `HTTP_HOST` a nginx deve conservare la porta
`docker/nginx/default.conf` rimette `fastcgi_param HTTP_HOST $http_host;` dopo
l'`include fastcgi_params`. Non e' ridondanza: dalla 1.26 il `fastcgi_params` di
sistema usa `$host`, che la porta la butta via. Con `APP_URL` su porta non
standard — cioe' lo sviluppo locale su `:8090` — Laravel ricostruisce l'URL
della richiesta senza porta mentre `URL::forceRootUrl()` genera i link con, e
ogni URL firmato non valida piu': **tutti** gli upload di file rispondono 401,
perche' l'endpoint di Livewire aborta sulla firma. In produzione `APP_URL` non
ha porta e le due forme coincidono, quindi il guasto si vede solo in locale.
Inoltrare l'header grezzo e' sicuro perche' `trustHosts()` in `bootstrap/app.php`
rifiuta gia' gli Host che non siano quello di `APP_URL`, `localhost` o
`127.0.0.1`.
### Modifica/cancellazione delle assegnazioni: solo amministratore
@@ -918,7 +1025,7 @@ blocchi che impedivano di scrivere il primo test sono stati rimossi: stage
docker compose --profile test run --build --rm test
```
**Coperto oggi — 233 test, tutti verdi:**
**Coperto oggi — 294 test, tutti verdi:**
| File | Cosa protegge |
|------|---------------|
@@ -929,6 +1036,10 @@ docker compose --profile test run --build --rm test
| `tests/Feature/PaginaBackupImportTest.php` | La pagina servita per intero: permessi, le cinque zone con la loro ancora **e nel loro ordine**, la console unica, l'assenza dei vecchi riquadri di log, che nessun tag `<x-btn` sia rimasto da compilare, che i pulsanti usino il sistema unico, e che la console sia definita prima del pacchetto che avvia Alpine |
| `tests/Feature/PagineApplicativeTest.php` | **Ogni** pagina dell'applicazione si apre davvero (200) e non ha markup rimasto da compilare. È la rete che serve quando una modifica tocca tutte le viste in una volta: un errore di markup non rompe niente finché qualcuno non apre quella pagina |
| `tests/Unit/StileUnificatoTest.php` | Che i vocabolari di stile paralleli non rientrino: `.btn-action*`, `.btn-primary-*`, l'hover simulato con `onmouseover`, gli sfondi scritti a mano sui pulsanti, le classi `tm-btn` messe in una vista invece che dal componente |
| `tests/Feature/PiantineManagerTest.php` | La pagina di gestione: accesso riservato, KML accettato e rifiutato, collegamento e scollegamento di una piantina, rifiuto di un territorio che ne ha gia' una, cancellare il territorio **non** cancella il disegno (la regressione che la cascata causava a ogni import XML), niente PDF senza territorio, e l'anteprima che non tocca `pdf_path` ne' lo storico |
| `tests/Feature/PiantineEditorTest.php` | L'editor: piantina salvata senza territorio, territorio creato dalla scheda con zona/tipologia/confini, aggiornamento di uno esistente, zona e tipologia create sul posto e lasciate selezionate, numero e zona duplicati respinti, ridisegno che sostituisce senza duplicare, geometria non valida ignorata |
| `tests/Unit/PiantinaPdfServiceTest.php` | La cartolina: due pagine, la Localita' presa dalla zona e i confini davvero presenti nel PDF (estratti decomprimendo i flussi, non solo "non ha sollevato eccezioni"), il giro completo dello storico — sostituzione, ripristino reversibile, file sparito |
| `tests/Unit/PiantinaRendererTest.php` | Il client del renderer: formato, qualita' e lato massimo davvero spediti, JPEG salvato con l'estensione giusta, il file di un formato precedente rimosso invece che lasciato orfano, errori e risposte non-immagine trasformati in messaggi leggibili, geometria corrotta bloccata prima della chiamata |
| `tests/Feature/HomeMediaDurataTest.php` | La "media percorrenza congregazione": unità di misura, esclusione delle assegnazioni aperte, dei rientri nello stesso giorno, delle date impossibili ereditate dalla vecchia applicazione e degli altri anni di servizio |
Il primo giro ha già trovato tre difetti reali, tutti corretti: il warning di
@@ -1007,9 +1118,12 @@ questi elementi si può mettere a fuoco — ma sono lo stesso genere di debito:
stesso verde compare in quattro tonalità leggermente diverse, e cambiare la
tinta dell'applicazione oggi significa cercare a mano tutte le occorrenze.
La via è la stessa già percorsa: dei token in `app.css` (o le variabili di tema
di Tailwind 4) e le viste che li citano invece di ripetere i valori. Il test
`StileUnificatoTest` è il posto dove aggiungere la guardia, quando si farà.
**Dal 28/08 metà del lavoro è fatta**: i token esistono (`:root` e `@theme` in
`app.css`, vedi *Il sistema visivo sta nei token*), e le scale `gray`/`indigo`
di Tailwind sono ridefinite, quindi tutto ciò che usa le utility è già
allineato. Resta da togliere gli `style=` con gli esadecimali scritti a mano e
farli citare i token. Il test `StileUnificatoTest` è il posto dove aggiungere la
guardia, quando si farà.
Fuori perimetro per scelta: `resources/views/pdf/*` le impagina dompdf, che di
Tailwind e delle nostre classi non sa niente. `StileUnificatoTest` esclude anche
@@ -1018,6 +1132,22 @@ 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
`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.
---
## Trappole da conoscere