++ fix e controlli

This commit is contained in:
2026-07-30 17:05:39 +02:00
parent 712336d652
commit 440d63a6b9
54 changed files with 4921 additions and 1322 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: **29 luglio 2026**.
Ultimo aggiornamento: **30 luglio 2026**.
---
@@ -22,18 +22,36 @@ 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 | ❌ **assenti** — vedi [Cosa resta aperto](#cosa-resta-aperto) |
| Test automatici | ⚠️ **impalcatura pronta, 57 test** — copertura ancora parziale |
**Punto in cui ci si è fermati (29/07/2026).** Su `develop` è pubblicato tutto:
**Punto in cui ci si è fermati (30/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.
di avanzamento reali e console di log (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*.
Il 30/07 sono state fatte due cose.
**La suite di test, che prima non esisteva**: 79 test eseguibili con un comando,
senza dipendere da altri servizi. Sono **verdi** ma coprono ancora una frazione
della logica di business — vedi *Cosa resta aperto*. Non fanno parte
dell'immagine di produzione e non girano su un deploy: sono uno strumento
locale.
**La pagina "Backup e import" è stata riorganizzata**: cinque zone numerate con
un indice che ci salta sopra evidenziandole, un solo sistema di pulsanti (con
gli stati di focus, che prima non c'erano), e **una sola console** per tutte le
operazioni, agganciata in fondo alla finestra, al posto dei cinque riquadri di
log sparsi. L'azzeramento è diventato un componente a sé per poter stare in
fondo alla pagina. Tutto verificato lato server dai test; **il comportamento nel
browser — apertura automatica della console, scorrimento, lampeggio delle zone —
non è stato provato**, perché richiede un browser: è la prima cosa da guardare
aprendo la pagina.
Il ripristino dall'interfaccia resta impraticabile per pacchetti grandi finché
non si decide come aggirare Cloudflare: è la prima voce di *Cosa resta aperto*
e l'unica che richiede una decisione, non del lavoro.
---
@@ -556,6 +574,228 @@ Tre scelte non ovvie:
hash generato con `BCRYPT_ROUNDS` diverso) ed evita che Eloquent sovrascriva i
timestamp importati. Stessa tecnica in `applyRowMeta()` per le altre entità.
### La pagina "Backup e import": cinque zone e una sola console
La pagina era cresciuta per accumulo: sette riquadri senza un ordine dichiarato,
la zona pericolosa a metà strada, **cinque** aree di log separate e pulsanti che
ogni sezione ridefiniva per conto proprio. È stata rimessa in ordine attorno a
tre regole.
**Cinque zone numerate, in ordine di quanto sono definitive.** `1 Backup`,
`2 Ripristino`, `3 Import PDF`, `4 XML`, `5 Zona pericolosa`. Ognuna ha
un'ancora (`id="zona-…"`) e l'indice appiccicato in cima ci salta sopra. Un test
verifica che le cinque ancore esistano *e che siano in quell'ordine nel
sorgente*: l'ordine è la sostanza della sistemazione, non un dettaglio estetico.
**Il "focus" della pagina.** Premuto un pulsante — o scelta una voce
dell'indice — la pagina si sposta sulla zona di riferimento e la fa lampeggiare
(`window.tmFocusZona`). Il focus da tastiera si sposta **solo** quando la
richiesta è di navigazione: spostarlo anche a ogni azione strapperebbe il
cursore da sotto le dita di chi ha appena premuto. Il primo comando di ogni zona
è marcato `data-zona-focus` ed è quello che lo riceve.
**Una sola console, agganciata in fondo alla finestra.** Prima la stessa
operazione si raccontava in tre punti diversi: un ripristino cominciava nel
riquadro "Browser", proseguiva in quello "Server" e aveva un terzo "Dettaglio"
richiudibile nel pannello di avanzamento, in cima. Per capire dove si era
fermato bisognava guardarli tutti. Adesso c'è una cronologia sola, in ordine di
tempo, con la sorgente accanto a ogni riga, e la finestra è *fissa*: quasi tutti
i modi in cui un caricamento muore sono muti, e l'unico indizio è dove si è
interrotto il flusso di righe — un log che esce dallo schermo appena si scorre
non serve proprio nel momento in cui servirebbe.
Dettagli non ovvi della console:
- **Le righe del server viaggiano già scremate.** Il conto di quante ne sono
state spedite lo tiene il componente Livewire (`logSpediti`), non il browser:
la coda intera dentro ogni risposta significherebbe sessanta righe poco meno
di due volte al secondo, e per l'import PDF — la cui coda non ha tetto —
l'intera cronologia a ogni poll.
- **Per ogni operazione c'è un solo componente che la racconta**
(`SegueOperazioneBackup::tipiSeguiti()`). Backup e zona pericolosa guardano lo
stesso stato condiviso: senza un proprietario dichiarato spedirebbero entrambi
le stesse righe, e la console le mostrerebbe due volte.
- **Il download automatico si ascolta in un posto solo.** `scarica-backup` è un
evento su `window`: due componenti che lo ascoltassero entrambi assegnerebbero
`window.location.href` due volte, e il pacchetto verrebbe scaricato due volte.
L'ascolto sta nella console, che è l'unico elemento sempre presente. Per lo
stesso motivo `scaricaOra()` esce subito se non è il proprietario a chiamarlo:
il segno "download già inviato" sta nello stato condiviso, quindi chi segue
l'operazione di riflesso lo consumerebbe per primo — l'archivio sarebbe pronto
e non arriverebbe a nessuno, e per l'azzeramento è pure il passo che sblocca
la cancellazione.
- **Lo scorrimento automatico si sospende se si torna indietro a leggere.**
Trascinare in fondo mentre si sta guardando una riga di dieci secondi prima è
il modo più rapido di rendere inutile un log che scorre. Compare invece
"Torna in fondo".
- **Il pannello è a posizione fissa, quindi la pagina ha uno spaziatore in
fondo.** Senza, coprirebbe l'ultima zona — che è quella pericolosa.
- **Il JavaScript della console sta in un `<script>`, non dentro `x-data`.** È
la via d'uscita dalla trappola documentata più sotto: dentro un `<script>` le
virgolette doppie e gli apostrofi non hanno alcuna limitazione. Non sta in
`resources/js/app.js` perché quel bundle è un modulo ES, che il browser rimanda
a dopo il parsing del documento, mentre Alpine arriva col pacchetto di Livewire
in fondo al `<body>` ed è già avviato a quel punto: una `Alpine.data()`
registrata dopo l'avvio non viene applicata agli elementi già inizializzati. Un
`<script>` classico nella pagina viene eseguito prima, sempre — e c'è un test
che verifica proprio che la definizione preceda `livewire.js`, perché è un
rapporto che guardando i due file non si vede.
**Un solo sistema di pulsanti** — vedi la sezione dedicata qui sotto.
### Un solo pulsante per tutta l'applicazione
`resources/css/app.css` definisce `.tm-btn`;
`resources/views/components/btn.blade.php` è l'unico posto da cui si usa. Nessuna
vista scrive quelle classi a mano, e nessuna vista definisce più un pulsante per
conto proprio.
Da dove veniamo: **quattro vocabolari in circolazione insieme** — `style=`
inline con i colori scritti a mano (più `onmouseover`/`onmouseout` per simulare
l'hover), catene di utility Tailwind ripetute a memoria, `.btn-action*` in sei
tinte e `.btn-primary-*` in due. La stessa azione cambiava aspetto da una pagina
all'altra, l'ordine di *Salva* e *Annulla* si invertiva fra un form e l'altro, e
**nessuno** di quei pulsanti aveva uno stato di focus: chi naviga da tastiera non
vedeva dove si trovava.
Il sistema ha due assi ortogonali, non una tavolozza:
| Asse | Valori | Regola |
|------|--------|--------|
| `variante` | `primary` | l'azione principale. Una sola per schermata o zona. |
| | `secondary` | tutto il resto: dettaglio, modifica, scarica, filtra. |
| | `danger` | **solo** dove si cancellano o si sovrascrivono dati. |
| | `ghost` | annulla, chiudi, pulisci. |
| `tono` | `solido` | il pieno (default), per le azioni isolate. |
| | `tenue` | la tinta chiara, per le righe di elenco. |
| | `velato` | per i comandi sopra una superficie colorata: solo la testata. |
più `dimensione="sm"` per le righe e `icona` per i pulsanti di sola icona.
Le decisioni non ovvie prese convertendo:
- **`tenue` esiste per le liste.** In una riga i pulsanti sono tanti quanti i
record: cinquanta pulsanti pieni sono rumore, e il rosso pieno smette di
significare "attenzione" quando compare cinquanta volte di fila.
- **Il rosso è tornato a significare una cosa sola.** *Rientra* era rosso pieno
quanto un *Elimina*, in tre pagine diverse; anche *Scarica PDF* nella scheda
"Da rientrare" della dashboard lo era, solo perché lo è la scheda che lo
contiene. Nessuno dei due cancella niente.
- **Ordine unico: azione principale prima, uscita dopo.** Valeva già in metà dei
form, l'opposto nell'altra metà, e nelle modali del Registro c'era la terza
variante. Ora è lo stesso ovunque, form e modali comprese.
- **`icona` richiede `aria-label`.** I pulsanti di sola icona avevano solo un
`title`, che i lettori di schermo annunciano in modo inaffidabile: un pulsante
senza nome accessibile è un pulsante che non si può usare senza vederlo.
- **`velato` non è una quinta variante inventata.** La testata ha il suo
gradiente, e lì un pulsante bianco o indaco sarebbe una toppa: il colore lo dà
lo sfondo. Ha anche un anello di focus bianco, perché quello blu su indaco non
si distingue.
Attenzione a una trappola di Blade: dentro un tag di componente si scrive
`:disabled="$condizione"`, **non** `@disabled($condizione)`. Le direttive non
vengono compilate lì dentro: il tag `<x-btn>` resta letterale nel PHP generato
mentre il `</x-btn>` viene chiuso, e la vista muore con «syntax error, unexpected
token endif».
A tenere insieme tutto questo ci sono due test, perché una convenzione senza
guardia dura fino alla prossima pagina scritta copiando una vecchia:
- `tests/Unit/StileUnificatoTest.php` legge i sorgenti delle viste e rifiuta il
ritorno di `.btn-action*`, `.btn-primary-*`, `onmouseover`/`onmouseout`, degli
sfondi scritti a mano su `<button>`/`<a>`, e delle classi `tm-btn` messe
direttamente in una vista invece che dal componente.
- `tests/Feature/PagineApplicativeTest.php` apre **ogni** pagina e verifica che
non sia rimasto un `<x-btn` da compilare — perché un componente non compilato
non solleva: semplicemente il pulsante non c'è.
### Perché l'azzeramento è un componente a sé
`Settings/ZonaPericolosa` è nato da un problema di sola disposizione: la zona
pericolosa deve stare **in fondo** alla pagina, mentre backup e ripristino stanno
in cima, e i tre erano lo stesso componente. Reso da `BackupManager` l'azzeramento
finiva a metà pagina, fra il ripristino e l'import — l'ultimo posto in cui si
vuole trovare un pulsante che cancella tutto.
La logica condivisa non è stata duplicata: sta nel trait
`Livewire/Concerns/SegueOperazioneBackup` (posto unico, aggancio all'operazione
in corso, download automatico, righe verso la console). Duplicare `postoLibero()`
avrebbe significato duplicare la regola su cui si regge il fatto che due
operazioni non si sovrascrivano: è l'ultimo posto dove ha senso avere due copie.
Conseguenza da conoscere: **due componenti pollano lo stesso stato**. Il
proprietario a 750ms (5s in attesa-download), l'altro a 3s — non ha righe da
raccontare, ma deve accorgersi che il posto si è liberato, altrimenti il suo
pulsante resterebbe disabilitato fino al ricaricamento della pagina, con l'aria
di essersi rotto. Si avvisano anche a evento (`operazione-avviata`), indirizzato
con `->to()` e non gridato: senza, tornerebbe pure al mittente.
Il secondo tempo dell'azzeramento (verifica del backup, password, conferma)
compare adesso **dentro la zona**, dove è stato premuto il primo. Prima usciva
nel pannello di avanzamento in cima, a schermate di distanza dal pulsante che lo
aveva avviato.
### La suite di test ha un'immagine sua
Le dipendenze di sviluppo non stanno nell'immagine di produzione, e non devono
cominciare a starci: `--no-dev` è la ragione per cui il runtime non porta con sé
PHPUnit, Faker e Ignition. Da qui uno stage `test` nel `Dockerfile`, che
condivide con `runtime` lo stage `base` — stesse estensioni, stesso `php.ini` —
e si distingue solo per il `vendor/` completo. Un test che passa lì dentro non
può passare grazie a un ambiente diverso da quello vero.
Le scelte che vale la pena conoscere:
- **`runtime` deve restare l'ultimo stage del file.** `docker build` senza
`--target` costruisce l'ultimo, ed è così che lo invocano sia i tre servizi
del compose (nessuno dichiara `target`) sia Dokploy. Uno stage aggiunto in
fondo diventerebbe **in silenzio** l'immagine di produzione. Per questo `test`
sta *prima* di `runtime` pur essendo stato aggiunto dopo. L'avvertenza è anche
nel Dockerfile, sopra lo stage.
- **La suite non dipende da niente.** `phpunit.xml` impone SQLite in memoria,
cache `array` e code `sync`, e il servizio compose non ha `depends_on` né
`env_file`. Gira a stack spento, non tocca `db_data` né `storage_data`, e
soprattutto non può puntare per sbaglio al database di sviluppo: un `.env`
ereditato dal container è esattamente il modo in cui una suite di test
cancella dati veri.
Il rovescio: **SQLite non è MariaDB**. Resta un solo punto non verificabile
qui, l'ordinamento di `Registro::render()`, che usa `CAST(... AS UNSIGNED)`:
va provato sullo stack vero. Gli altri due sono stati tolti di mezzo perché
costavano poco e in cambio rendevano testabile una pagina intera — la
migrazione `2026_04_08_123500`, il cui `GREATEST(...)` faceva proprio morire
la costruzione dello schema, e la media di percorrenza in `Home`, che usava
`DATEDIFF`/`YEAR` e teneva fuori dalla suite la pagina più visitata
dell'applicazione. Entrambe danno lo stesso risultato di prima.
- **Il sorgente non è montato, e il `--build` non è facoltativo.** Con i bind
mount la stessa suite passa da ~15 secondi a ~2 minuti (ogni bootstrap di
Laravel legge una decina di file attraverso il layer di Docker Desktop), e in
cambio si guadagna solo di non ricostruire. Il comando canonico è
`docker compose --profile test run --build --rm test`: senza `--build` si
eseguono i test dell'immagine precedente, che possono essere di due modifiche
fa. Stesso genere di trappola del "Rebuild deployment" su Dokploy.
- **`APP_KEY` di test è fissa e sta in chiaro in `phpunit.xml`.** Nome e cognome
dei proclamatori hanno il cast `encrypted`: senza chiave nemmeno un test che
non parla di cifratura riesce a scrivere una riga. Non ha alcun rapporto con
la chiave di produzione, che vive nell'ambiente e nel volume.
- **`righeS13()` si chiama in riflessione.** È privata di proposito (non è
un'azione Livewire) ma è il calcolo, non il PDF, quello che va protetto:
renderla pubblica solo per poterla testare avrebbe allargato la superficie
del componente per comodità del test.
### Un solo container esegue le migrazioni
Solo `app` migra. `queue-worker` e `scheduler` usano `queue-entrypoint.sh`, che
@@ -596,6 +836,10 @@ Contesto utile se ti imbatti in comportamenti che sembrano regressioni.
| 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` |
| Warning di `addFile()` al posto del messaggio | Su file illeggibile l'amministratore leggeva `ZipArchive::addFile(): No such file or directory` invece della frase che dice quale file e cosa controllare | `Services/BackupArchive` |
| `GREATEST()` in migrazione | Sintassi MySQL: lo schema non si costruiva su SQLite, quindi la suite di test non poteva partire | migrazione `2026_04_08_123500` |
| `@disabled` dentro `<x-btn>` | Direttiva non compilata: il tag restava letterale e la pagina moriva con «unexpected token endif» | viste di `settings` |
| Segno del download consumato dal componente sbagliato | L'archivio pronto non sarebbe arrivato a nessuno — e per l'azzeramento è il passo che sblocca la cancellazione | `SegueOperazioneBackup::scaricaOra()` |
---
@@ -623,39 +867,60 @@ 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. Copertura dei test ancora parziale
`composer.json` richiede PHPUnit ma **la cartella `tests/` non esiste** e non
c'è `phpunit.xml`. Ogni verifica è manuale. È il debito più rilevante: le regole
di business (anno teocratico, stato territorio, priorità automatica, percentuale
campagne) sono esattamente il tipo di logica che va protetta da test — e a
maggior ragione backup, ripristino e azzeramento, dove un errore non si vede
finché non è troppo tardi.
L'impalcatura ora c'è (vedi *La suite di test ha un'immagine sua*) e i tre
blocchi che impedivano di scrivere il primo test sono stati rimossi: stage
`test` nel Dockerfile con le dipendenze di sviluppo, connessione `sqlite` in
`config/database.php`, `phpunit.xml`. Si lancia con:
Tre blocchi concreti da rimuovere prima di poter scrivere il primo test:
```bash
docker compose --profile test run --build --rm test
```
1. **PHPUnit non è nell'immagine.** `docker/php/Dockerfile` installa con
`--no-dev` (giusto, per la produzione), quindi `vendor/phpunit` nel container
non c'è e `docker compose exec app ./vendor/bin/phpunit` non funziona.
Servono o un `composer install` locale sull'host, o uno stage `dev` nel
Dockerfile che tenga le dipendenze di sviluppo.
2. **Manca la connessione SQLite.** `config/database.php` ridefinisce
`connections` e lascia solo `mysql`. Per usare SQLite in memoria va
riaggiunta la connessione `sqlite`.
3. **Manca `phpunit.xml`.** L'autoload `Tests\` in `composer.json` c'è già, il
resto no.
**Coperto oggi — 233 test, tutti verdi:**
Dove conviene cominciare, in ordine di rapporto valore/attrito — i primi due
non hanno bisogno del database e quindi si sbloccano risolvendo solo il punto 1:
| File | Cosa protegge |
|------|---------------|
| `tests/Unit/BackupArchiveTest.php` | Round-trip crea → riestrai, manifest, avanzamento fino al totale, fallimento su file illeggibile con archivio rimosso, rifiuto di manifest mancante/illeggibile/di formato futuro e di `data.xml` mancante o vuoto, zip slip (percorso assoluto, `..`, unità Windows) e normalizzazione dei percorsi validi, staging ripulito dopo un fallimento, sostituzione totale del disco al ripristino, svuotamento con conteggio |
| `tests/Unit/BackupOperationStateTest.php` | Il posto unico: occupazione, rilascio a completamento e a fallimento, puntatore orfano, operazione già chiusa, chiusura tardiva che non deve liberare il posto della nuova, attesa-download che lo tiene occupato, azzeramento della barra al cambio fase, coda dei log limitata |
| `tests/Feature/RegistroS13Test.php` | Il ciclo da quattro gruppi del modulo S-13 (1/3/4/5/8/9 assegnazioni), la colonna "Restituito l'ultima volta il" compreso il caso in cui l'ultima del giro precedente è ancora aperta, esclusione di territori disattivati e cestinati, filtro sull'anno di servizio, ordinamento naturale dei numeri |
| `tests/Feature/ZonaPericolosaTest.php` | I due tempi dell'azzeramento: frase di conferma, backup preventivo senza cancellazione, rifiuto se il posto è occupato, conferma senza archivio servito, senza password, con password sbagliata, blocco dopo cinque tentativi, annullamento che libera il posto lasciando il backup valido. In più: che ogni operazione abbia un solo componente a raccontarla, e che il segno del download non venga consumato da chi non la possiede |
| `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/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 |
- `Services/BackupArchive` con `Storage::fake('public')`: round-trip
crea → verifica → estrai, rifiuto dello zip slip, rifiuto del formato futuro,
fallimento su archivio incompleto.
- `Services/BackupOperationState` con il driver di cache `array`: il posto
unico, il rilascio alla chiusura, il puntatore orfano, il non-rilascio da
parte di un'operazione vecchia.
- `Livewire/Registro::righeS13()`: il ciclo da quattro e la colonna del
riporto, con i casi 2/3/4/5/8/9 assegnazioni (questi sì, con database).
Il primo giro ha già trovato tre difetti reali, tutti corretti: il warning di
`ZipArchive::addFile()` che copriva il messaggio d'errore vero, il `GREATEST()`
in migrazione, e `@disabled` dentro un tag di componente (vedi *Bug corretti*).
Quello che questi test **non** possono coprire: Alpine e il comportamento della
console — apertura automatica, scorrimento, evidenziazione delle zone — girano
nel browser, e nella suite non c'è un browser. Quella parte va provata a mano.
**Scoperto ancora, in ordine di quanto costerebbe sbagliarsi:**
- `Services/BackupArchive::extract()` sui **limiti anti zip-bomb**: numero di
entry, dimensione della singola entry, totale decompresso. Sono tre `if` che
nessun test tocca, e il caso in cui contano non si presenta mai per sbaglio.
- `Jobs/RestoreBackupArchive` e `Jobs/ResetApplication` **nel loro insieme**:
l'ordine staging → database → file, il `failed()` che libera il posto, la
sopravvivenza dell'amministratore che ha chiesto l'azzeramento, il
`password_change_required` acceso su tutte le righe. È la logica il cui
errore si scopre solo quando è troppo tardi.
- `Livewire/Settings/XmlExchange`: il **round-trip** export → import senza
perdite, che è il presidio contro il tipo di bug già visto con `legacy_id`.
- Le regole di business: anno teocratico (`AnnoTeocratico::perData` a cavallo
del 1º settembre), `Territorio::stato` e la soglia `giorni_per_smarrito`,
priorità automatica, percentuale di percorrenza delle campagne.
- Il permesso come contratto: che un `assistente` **non** possa modificare o
eliminare un'assegnazione, una campagna, un territorio o un proclamatore. È
scritto in tre posti (rotta, componente, vista) e un test di richiesta lo
verificherebbe tutto insieme.
Attenzione a non scrivere test su ciò che SQLite non sa eseguire: la dashboard
(`Home`) e l'ordinamento di `Registro::render()` usano SQL specifico di MySQL.
### 2. Race su doppia assegnazione
@@ -688,6 +953,28 @@ limit. Il controllo d'accesso è solido (confronto in tempo costante su codice a
~62 bit, più controlli di scadenza), ma un tentativo di enumerazione non
verrebbe né rallentato né registrato. Un `throttle:30,1` costerebbe una riga.
### 6. Restano da uniformare i colori che non sono pulsanti
I pulsanti sono a posto in tutta l'applicazione (vedi *Un solo pulsante per
tutta l'applicazione*), ma i **colori decorativi** sono ancora scritti a mano,
esadecimale per esadecimale, dentro gli attributi `style=`: le piastrelle delle
icone e le fasce delle schede nella dashboard, gli sfondi delle intestazioni di
tabella, i pannelli delle modali del Registro, i riquadri dei messaggi nel
layout.
Non sono un problema di accessibilità come lo erano i pulsanti — nessuno di
questi elementi si può mettere a fuoco — ma sono lo stesso genere di debito: lo
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à.
Fuori perimetro per scelta: `resources/views/vendor/pagination/*` sono viste
pubblicate da Laravel, e `resources/views/pdf/*` le impagina dompdf, che di
Tailwind e delle nostre classi non sa niente.
---
## Trappole da conoscere
@@ -718,7 +1005,11 @@ 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.**
**Il JavaScript che vive dentro un attributo HTML.** Vale ancora per i blocchi
`x-data` di `backup-manager`, `zona-pericolosa` e della zona Import PDF, che
gestiscono i caricamenti. La console, che era il blocco più grosso, è stata
portata fuori in un `<script>` proprio per uscirne (vedi *La pagina "Backup e
import"*); il resto resta soggetto alla regola.
`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
@@ -770,7 +1061,13 @@ cp .env.example .env
# SESSION_SECURE_COOKIE=false, e le tre password
docker network create dokploy-network
# 0. La suite passa (non serve nient'altro acceso, ~15 s)
docker compose --profile test run --build --rm test
# 1. L'immagine builda
# Senza --target si costruisce l'ultimo stage, che e' `runtime`: se questo
# comando producesse un'immagine con phpunit dentro, qualcuno ha aggiunto
# uno stage in fondo al Dockerfile.
docker build -f docker/php/Dockerfile -t termanager2-check .
# 2. Stack pulito da zero
@@ -795,8 +1092,9 @@ docker compose exec app php artisan tinker --execute="
"
```
È lo stesso file che gira in produzione: l'unica differenza è il profilo `dev`
(che aggiunge Mailpit) e i valori nel `.env`.
È lo stesso file che gira in produzione: le uniche differenze sono i profili
`dev` (che aggiunge Mailpit) e `test` (che aggiunge la suite, effimera) e i
valori nel `.env`.
---