From 712336d6522cde3d1510c201a83bd54a926cd62f Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Wed, 29 Jul 2026 18:33:30 +0200 Subject: [PATCH] ++ update HANDOFF.md --- HANDOFF.md | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++++- README.md | 15 +++++++ 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/HANDOFF.md b/HANDOFF.md index e3a0f7c..0a82df8 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -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 `