++ update HANDOFF.md

This commit is contained in:
2026-07-29 18:33:30 +02:00
parent f909791378
commit 712336d652
2 changed files with 141 additions and 1 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: **25 luglio 2026**.
Ultimo aggiornamento: **29 luglio 2026**.
---
@@ -20,8 +20,21 @@ redeploy, login, tutte le pagine, code e scheduler.
| Primo avvio a zero-config | ✅ migrazioni + seed + admin automatici |
| Redeploy senza interventi | ✅ ~25 secondi |
| 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 | ❌ **assenti** — vedi [Cosa resta aperto](#cosa-resta-aperto) |
**Punto in cui ci si è fermati (29/07/2026).** Su `develop` è pubblicato tutto:
backup completo `.zip`, ripristino, azzeramento, cambio password forzato, barre
di avanzamento reali e console di log (ultimo commit `f909791`, deploy
verificato confrontando l'impronta degli asset). `MAX_UPLOAD_MB=2048` è
impostato **solo** su develop; la produzione non è stata toccata e resta sul suo
deploy precedente, senza quella variabile.
Il ripristino dall'interfaccia resta però impraticabile per pacchetti grandi
finché non si decide come aggirare Cloudflare: è la prima voce di *Cosa resta
aperto*.
---
## Da dove veniamo
@@ -37,6 +50,42 @@ Da qui la ristrutturazione descritta sotto.
---
## Dove gira davvero
Due ambienti su Dokploy, stesso progetto `TerManager2`, entrambi alimentati dal
repository Gitea `francesco/termanager2` con **autoDeploy attivo**.
| Ambiente | Branch | Dominio | Compose (`appName`) |
|--------------|--------|-------------------------------------|----------------------------|
| `production` | `main` | `termanager2.pyconetwork.it` | `termanager2-core-vizcxr` |
| `develop` | `dev` | `demo-termanager.pyconetwork.it` | `termanager2-core-vbekis` |
Conseguenze pratiche, tutte imparate sul campo:
- **Il branch decide l'ambiente.** Pubblicare su `dev` non tocca la produzione,
che segue `main`. È la separazione su cui contare quando si vuole provare
qualcosa senza rischi.
- **Un "Rebuild deployment" NON scarica il codice nuovo**: ricostruisce il
checkout già presente e ci mette una ventina di secondi. Per prendere l'ultimo
commit serve un deploy vero (`compose.deploy` via API, o *Deploy*
nell'interfaccia). Un rebuild che finisce troppo in fretta è il segnale che si
sta ricostruendo il codice di prima.
- **Le variabili d'ambiente stanno in Dokploy, non nel repository.** Il `.env`
dell'ambiente lo genera l'interfaccia: una variabile aggiunta a
`.env.example` non arriva da sola sui deployment, va aggiunta a mano lì. È
successo con `MAX_UPLOAD_MB`, presente in develop e — deliberatamente — non in
produzione.
- **Verifica che il codice servito sia quello giusto** confrontando l'impronta
degli asset: `public/build/assets/app-*.css` e `-*.js` hanno il nome derivato
dal contenuto, quindi se coincide con quello della propria build, il frontend
in linea è lo stesso sorgente. È il controllo più rapido dopo un deploy, e non
richiede di autenticarsi.
---
## Decisioni architetturali
Le scelte non ovvie, con il motivo. Se una di queste ti sembra sbagliata, il
@@ -378,6 +427,16 @@ Scelte non ovvie:
più nulla per sempre), e il codice HTTP dentro il messaggio, perché 413 e 419
non hanno un corpo JSON da cui ricavare una spiegazione.
- **La console di log non è un vezzo diagnostico.** Quasi tutti i modi in cui un
caricamento muore sono muti: Cloudflare o nginx chiudono la connessione e
nessun evento del browser porta con sé una spiegazione. Il pannello *Console*
nella scheda di ripristino registra file scelto e dimensione, limite del
server, avanzamento ogni 10%, codice HTTP della risposta, errori, timeout e
interruzioni; sotto, i log lato server dell'operazione. Un battito ogni 15
secondi scrive «nessun avanzamento da N s», che è l'unico modo di distinguere
*lento* da *fermo*. Il pulsante *Copia tutto* esiste perché il primo passo di
ogni diagnosi è farsi mandare quel testo.
- **Nell'azzeramento i file si cancellano prima del database.** Svuotare il
disco è l'unico passo che può fallire per cause esterne (permessi sul
volume), e fallire lì non ha ancora toccato niente. Nell'ordine inverso un
@@ -530,6 +589,13 @@ Contesto utile se ti imbatti in comportamenti che sembrano regressioni.
| Zip senza limiti | Zip bomb → esaurimento memoria | `TerritorioPdfImportDispatcher` |
| Export XML senza `legacy_id` | Il re-import dell'export dell'app corrente perdeva territori e assegnazioni (FK non risolte) | `Livewire/Settings/XmlExchange` (`addLegacyIdAttribute`) |
| Migrazioni thumbnail duplicate | `migrate:rollback` falliva a metà | rimosse 2 migrazioni ridondanti |
| `addFile()` senza controllo esito | Backup con un file mancante, **silenzioso**: ci si accorge solo al ripristino | `Services/BackupArchive` |
| Esito del ripristino con `Cache::pull` | L'amministratore non vedeva **mai** il riepilogo: la `fetch` che segue il 302 lo consumava prima della navigazione | `Auth/PasswordChangeController` |
| Nessun blocco fra operazioni | Backup, ripristino e azzeramento potevano sovrapporsi sullo stesso database e disco | `Services/BackupOperationState` (`corrente()`) |
| Job ucciso = operazione eterna | Timeout o crash del worker lasciavano lo stato "in corso" e il posto occupato | `failed()` sui tre job di backup |
| Cancellazione file senza verifica | «N file cancellati» con i file ancora sul disco (permessi) | `BackupArchive::svuotaDiscoPubblico()` |
| Eliminazione definitiva territorio | La riga spariva, mappa e miniatura restavano orfane sul volume per sempre | `Territori/TerritorioCestino` |
| Virgolette doppie in `x-data` | Attributo HTML chiuso a metà: il JavaScript finiva a video come testo | `backup-manager.blade.php` |
---
@@ -537,6 +603,26 @@ Contesto utile se ti imbatti in comportamenti che sembrano regressioni.
In ordine di priorità.
### 0. Il ripristino da browser è limitato a 100 MB — decisione in sospeso
**È il punto da cui riprendere.** Cloudflare rifiuta i caricamenti oltre 100 MB
(vedi *Trappole da conoscere*), quindi il ripristino dall'interfaccia non
funziona per un pacchetto con molte mappe. L'applicazione è a posto: il muro sta
davanti.
Da decidere, in ordine di rapidità:
1. Togliere il dominio da Cloudflare (DNS-only), anche solo su un sottodominio
dedicato all'amministrazione — gratis e immediato.
2. Caricare puntando direttamente all'IP dell'origine, scavalcando Cloudflare.
3. Ripristino dai volumi (procedura nel README): nessun limite, serve accesso
al server.
4. Piano Cloudflare Business: 200 MB, spesso ancora insufficienti.
Resta da accertare **quanto pesa davvero il pacchetto** che si vuole
ripristinare: sotto i 100 MB il problema non sarebbe la dimensione ma il timeout
di 100 secondi di Cloudflare, e la soluzione sarebbe un'altra.
### 1. Nessun test automatico
`composer.json` richiede PHPUnit ma **la cartella `tests/` non esiste** e non
@@ -606,6 +692,45 @@ verrebbe né rallentato né registrato. Un `throttle:30,1` costerebbe una riga.
## Trappole da conoscere
**Cloudflare taglia i caricamenti a 100 MB, e `MAX_UPLOAD_MB` non c'entra
niente.** I domini pubblici passano da Cloudflare, che sul piano free rifiuta i
corpi di richiesta oltre **100 MB**. Rifiuta sulla base del `Content-Length`,
quindi il `413` arriva dopo che il browser ha spedito appena un megabyte: la
barra di caricamento si pianta a una percentuale bassa e — senza i gestori
aggiunti alla console — non compare alcun messaggio. È **il** motivo per cui il
ripristino di un pacchetto grande sembra bloccarsi senza spiegazione.
Misurato il 29/07/2026 su `demo-termanager.pyconetwork.it`:
| Payload | Esito |
|---------|----------------------------------------------------|
| 150 MB | `413 Payload Too Large`, `Server: cloudflare`, 2,7 s |
| 100 MB | `524` (timeout dell'origine dopo 100 s di Cloudflare) |
Il pacchetto **non raggiunge mai nginx**, quindi alzare `MAX_UPLOAD_MB` non
sposta nulla: quel valore governa solo ciò che accade *dopo* Cloudflare. Le
strade per superarlo sono togliere il dominio da Cloudflare (DNS-only, anche
solo su un sottodominio d'amministrazione), caricare puntando direttamente
all'IP dell'origine, oppure usare il ripristino dai volumi. Il piano Business
arriva a 200 MB, che per un archivio con tutte le mappe è spesso ancora poco.
Nota sul `524`: anche restando sotto i 100 MB, Cloudflare chiude dopo 100
secondi se l'origine non ha ancora risposto. Un caricamento lento può quindi
fallire pur essendo dentro il limite di dimensione.
**Il JavaScript di `backup-manager.blade.php` vive dentro un attributo HTML.**
`x-data` finisce alla **prima virgoletta doppia**, comprese quelle dentro un
commento JavaScript: un banale `// un generico "errore"` ha chiuso l'attributo a
metà e riversato tutto il codice restante a video come testo — è successo due
volte. Nello stesso blocco, un `\'` scritto a mano nel sorgente Blade *chiude* la
stringa invece di proteggere l'apostrofo, perché fra il file e l'attributo non
c'è alcun livello di escape. Regole: nessuna virgoletta doppia in quel blocco,
da nessuna parte; apostrofi italiani solo dentro backtick.
Un blocco `<script>` con `@script` eviterebbe la questione alla radice, ma
**rompe il rilevamento della radice singola di Livewire** e restituisce 500 —
provato. Il file porta l'avvertenza in testa; leggerla prima di intervenire.
**`APP_KEY` è il singolo punto di perdita dati.** I nomi dei proclamatori sono
cifrati con questa chiave. Il backup del database senza la chiave è inutile per
quei campi. Se non la imposti, l'entrypoint ne genera una e la salva nel volume