++ 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 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. 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 | | Primo avvio a zero-config | ✅ migrazioni + seed + admin automatici |
| Redeploy senza interventi | ✅ ~25 secondi | | Redeploy senza interventi | ✅ ~25 secondi |
| Sviluppo locale | ✅ stesso compose, profilo `dev` | | 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) | | 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 ## 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 ## Decisioni architetturali
Le scelte non ovvie, con il motivo. Se una di queste ti sembra sbagliata, il 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 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. 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 - **Nell'azzeramento i file si cancellano prima del database.** Svuotare il
disco è l'unico passo che può fallire per cause esterne (permessi sul 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 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` | | 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`) | | 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 | | 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à. 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 ### 1. Nessun test automatico
`composer.json` richiede PHPUnit ma **la cartella `tests/` non esiste** e non `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 ## 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 **`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 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 quei campi. Se non la imposti, l'entrypoint ne genera una e la salva nel volume

View File

@@ -441,6 +441,21 @@ corpo su disco e PHP lavora su un file temporaneo — ma serve spazio libero nel
volume. Oltre qualche giga conviene comunque il backup dei volumi qui sotto: un volume. Oltre qualche giga conviene comunque il backup dei volumi qui sotto: un
upload lunghissimo da browser è fragile. upload lunghissimo da browser è fragile.
> **Se il dominio passa da Cloudflare, comanda Cloudflare.** Sul piano free
> rifiuta i corpi di richiesta oltre **100 MB**, e lo fa in base al
> `Content-Length`: il `413` arriva dopo appena un megabyte spedito, quindi il
> pacchetto **non raggiunge mai nginx** e `MAX_UPLOAD_MB` non sposta nulla.
> C'è anche un tetto di 100 secondi: un caricamento lento può fallire con `524`
> pur restando sotto il limite di dimensione.
>
> Per superarlo: togliere il dominio da Cloudflare (DNS-only), anche solo su un
> sottodominio d'amministrazione; oppure caricare puntando direttamente all'IP
> dell'origine; oppure usare il ripristino dai volumi qui sotto. Il piano
> Business arriva a 200 MB, spesso ancora insufficienti.
>
> La **Console** nella scheda di ripristino mostra il codice HTTP ricevuto: se
> leggi `413` con un pacchetto sotto `MAX_UPLOAD_MB`, il tetto è a monte.
### Zona pericolosa: azzera tutto ### Zona pericolosa: azzera tutto
In fondo alla stessa pagina, **Scarica backup e azzera tutto** riporta In fondo alla stessa pagina, **Scarica backup e azzera tutto** riporta