Files
termanager2/HANDOFF.md
francesco afaaeb5927 Semplifica il deploy: un solo docker-compose senza anchor YAML
I due file compose andavano tenuti allineati a mano, e gli anchor
(x-app-env, <<:) sono sintassi che va conosciuta per riuscire a leggere il
file. Per un deploy che deve restare semplice era complessita' non
giustificata.

Ora c'e' un solo docker-compose.yml, esplicito, che copre produzione e
sviluppo:

- ogni servizio dichiara quello che usa, senza merge key
- mailpit e' dietro il profilo "dev", quindi non parte mai in produzione
- la porta e' legata a 127.0.0.1 per default: in produzione il traffico
  arriva da Traefik sulla rete interna, in locale basta localhost:8080.
  Per esporla davvero: APP_PORT=0.0.0.0:8080
- .env diventa l'unica fonte di configurazione, senza valori ridichiarati

Compromesso accettato: sparendo i valori fissati nel compose, DB_HOST e
REDIS_HOST devono contenere i nomi dei servizi (mariadb, redis) e non un
indirizzo. E' segnalato in .env.example, nel README e in HANDOFF.md.

Per le modifiche al codice senza ricostruire l'immagine il README documenta
un docker-compose.override.yml, che Compose applica da solo ed e' gia'
ignorato da git.

Verificato: sei servizi healthy, login e pagine a 200, redeploy idempotente,
porta esposta solo su localhost, mailpit assente senza il profilo dev.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:41:48 +02:00

11 KiB

Handoff — TerManager2

Stato del progetto per chi ci mette mano dopo. Per installazione, deploy e uso quotidiano vedi README.md; qui c'è il contesto che il README non può dare: perché le cose sono come sono, e cosa resta aperto.

Ultimo aggiornamento: 18 luglio 2026.


Stato attuale

L'applicazione è pronta per il deploy in produzione su Dokploy. Lo stack è stato verificato end-to-end: build dell'immagine, primo deploy su volumi vuoti, redeploy, login, tutte le pagine, code e scheduler.

Aspetto Stato
Deploy Dokploy (Compose) ✅ verificato
Primo avvio a zero-config ✅ migrazioni + seed + admin automatici
Redeploy senza interventi ✅ ~25 secondi
Sviluppo locale ✅ stesso compose, profilo dev
Test automatici ❌ assenti — vedi Cosa resta aperto

Da dove veniamo

Il progetto aveva un difetto strutturale nel deploy: il Dockerfile compilava l'applicazione in /app-src, ma il runtime girava da /var/www/html, popolato solo dal bind mount del sorgente. L'immagine costruita non veniva mai usata. L'entrypoint rieseguiva composer install e npm run build a ogni avvio dentro il container. Su Dokploy, senza sorgente sull'host, significava build a runtime a ogni deploy: lento, dipendente dalla rete, e capace di fallire in produzione.

Da qui la ristrutturazione descritta sotto.


Decisioni architetturali

Le scelte non ovvie, con il motivo. Se una di queste ti sembra sbagliata, il contesto per ridiscuterla è qui.

L'immagine è autosufficiente

vendor/ e public/build sono compilati in stage separati del Dockerfile e copiati nell'immagine finale. I container di produzione non montano il sorgente e non eseguono build all'avvio.

Conseguenza pratica: dopo aver modificato dipendenze PHP o asset frontend, serve un rebuild dell'immagine, non un semplice restart.

nginx e php-fpm nello stesso container

Con container separati, nginx avrebbe bisogno di una copia di public/ e del volume storage solo per servire file statici. Per questa scala è complessità senza beneficio. Sono coordinati da supervisor (docker/supervisor/supervisord.conf), che abbatte il container se uno dei due muore definitivamente — altrimenti resterebbe "up" senza servire nulla.

La configurazione arriva dall'ambiente, non da .env riscritto

Il vecchio entrypoint riscriveva .env con sed a ogni avvio. Ora Laravel legge direttamente l'ambiente di processo, e docker-compose.yml si limita a env_file: .env senza ridichiarare nulla.

Il .env è quindi l'unica fonte di configurazione. Una versione precedente fissava nel compose i valori dettati dalla topologia (DB_HOST: mariadb e simili) perché vincessero su un .env sbagliato, ma richiedeva gli anchor YAML (x-app-env, <<:) ed è stata scartata a favore della leggibilità.

Il rovescio della medaglia: DB_HOST e REDIS_HOST devono valere mariadb e redis, cioè i nomi dei servizi. Se qualcuno vi mette un indirizzo IP il container non trova più il database. È documentato in .env.example e nel README, ma è il punto fragile di questa scelta.

Nota: perché la configurazione da ambiente funzioni, www.conf ha clear_env = no. Senza, php-fpm sanifica l'ambiente e Laravel non vedrebbe nessuna variabile.

Fail-fast sulle variabili mancanti

Lo stack si rifiuta di partire senza APP_URL, DB_PASSWORD, DB_ROOT_PASSWORD, REDIS_PASSWORD. Le password sono validate dall'interpolazione compose (${VAR:?}), APP_URL dall'entrypoint (dove il messaggio d'errore è più chiaro).

Deliberato: un errore immediato è meglio di un database in produzione con password secret.

Un solo container esegue le migrazioni

Solo app migra. queue-worker e scheduler usano queue-entrypoint.sh, che attende e non tocca lo schema. Due container in gara su artisan migrate è il modo tipico di ritrovarsi con uno schema applicato a metà.

APP_KEY risolta una volta sola all'avvio

Prima veniva rigenerata da AppServiceProvider::boot() a ogni richiesta se mancante — con il rischio di chiavi diverse a ogni richiesta e dati cifrati irrecuperabili. Ora è risolta dall'entrypoint (env → volume → generata) e il provider solleva un'eccezione se manca, invece di rigenerarla.


Bug corretti — i più significativi

Contesto utile se ti imbatti in comportamenti che sembrano regressioni.

Bug Effetto Dove
key:generate a runtime Rotazione silenziosa della chiave → perdita irreversibile dei dati cifrati AppServiceProvider
Import PDF da cartella mai accodato Upload completato, nessun territorio aggiornato, file orfani a crescita illimitata Livewire/Settings/XmlExchange
Permessi non riapplicati su Livewire Un permesso revocato restava attivo fino al reload della pagina AppServiceProvider (addPersistentMiddleware)
Host header injection Link generati (inclusi i PDF pubblici con codice d'accesso) verso domini arbitrari bootstrap/app.php, nginx/default.conf
Default TTL link mai convertito Link PDF validi 2 anni invece di 1 mese migrazione 2026_07_18_000001
syncPermissions() nel seeder Un re-seed azzerava i permessi personalizzati dei ruoli RolesAndPermissionsSeeder
campagna_id manipolabile Rientro conteggiato su una campagna arbitraria Livewire/Assegnazioni/Rientra (#[Locked])
pdftoppm senza timeout Un PDF malformato bloccava il worker per sempre TerritorioThumbnailService
Collisione nomi file PDF 1 A e 1-A scrivevano sullo stesso file → dati sovrascritti Jobs/ImportTerritoryPdfFolder
Zip senza limiti Zip bomb → esaurimento memoria TerritorioPdfImportDispatcher
Migrazioni thumbnail duplicate migrate:rollback falliva a metà rimosse 2 migrazioni ridondanti

Cosa resta aperto

In ordine di priorità.

1. Nessun test automatico

composer.json richiede PHPUnit ma la cartella tests/ non esiste. 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.

Nota: config/database.php ridefinisce connections e lascia solo mysql. Per usare SQLite in memoria nei test va riaggiunta la connessione sqlite.

2. Race su doppia assegnazione

Assegna::save() controlla che il territorio sia libero e poi crea la riga, ma nulla a livello di database impedisce due assegnazioni aperte sullo stesso territorio (doppio click, due schede). assegnazioneCorrente usa latestOfMany, quindi la seconda resta invisibile nell'interfaccia mentre falsa i conteggi.

Rimedio: transazione con lockForUpdate(), oppure un indice univoco su colonna generata che imponga una sola assegnazione aperta per territorio.

3. Scrittura durante il rendering

Assegnazione::shortPdfUrl() genera e salva il codice d'accesso al primo utilizzo, quindi una GET del registro può produrre decine di INSERT. Meglio generare il codice alla creazione dell'assegnazione (observer) e far restituire null al metodo quando manca.

4. Ricerca in memoria sui campi cifrati

Nome e cognome dei proclamatori sono cifrati, quindi la ricerca carica l'intera tabella e filtra in PHP (ProclamatoreIndex, Registro). Accettabile sotto qualche centinaio di record, non oltre. Se dovesse servire scalare: hash deterministico ricercabile affiancato al campo cifrato.

5. Endpoint PDF pubblici senza throttle

/p/{code} e le rotte del visualizzatore sono senza autenticazione né rate 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.


Trappole da conoscere

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 storage_data, stampandola nei log: se quel volume viene perso, i dati sono irrecuperabili.

APP_URL deve combaciare col dominio servito. L'applicazione rifiuta richieste con Host diverso (protezione host header injection) e restituisce 400 senza spiegazioni. È la causa più comune di "il deploy è andato ma il sito non si apre". localhost e 127.0.0.1 sono ammessi perché li usano gli health check interni.

Il volume storage_data non è solo cache. Contiene i PDF dei territori, le miniature e la chiave di fallback. Non va trattato come rigenerabile.

Modifiche a dipendenze o asset richiedono un rebuild. Non è più sufficiente riavviare il container: vendor/ e public/build vivono dentro l'immagine.


Come verificare che tutto funzioni

Dopo una modifica sostanziale, questo giro copre i percorsi critici:

C'è un solo docker-compose.yml, usato sia in produzione sia in locale. In locale serve creare una volta la rete esterna attesa da Dokploy.

cp .env.example .env
# nel .env: APP_URL=http://localhost:8080, APP_ENV=local,
#           SESSION_SECURE_COOKIE=false, e le tre password
docker network create dokploy-network

# 1. L'immagine builda
docker build -f docker/php/Dockerfile -t termanager2-check .

# 2. Stack pulito da zero
docker compose --profile dev down -v
docker compose --profile dev up -d --build
docker compose --profile dev ps     # tutti i servizi healthy

# 3. L'applicazione risponde
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/up      # 200
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/login   # 200

# 4. Il redeploy è idempotente
docker compose up -d --force-recreate app
docker compose logs app | grep -E "Migrations|Seed"
# atteso: "Migrations applied" + "Seed already applied"

# 5. La cifratura funziona
docker compose exec app php artisan tinker --execute="
  \$p = App\Models\Proclamatore::create(['nome'=>'Test','cognome'=>'Verifica','attivo'=>true]);
  \$raw = DB::table('proclamatori')->where('id',\$p->id)->value('nome');
  echo \$raw === 'Test' ? 'ERRORE: dati in chiaro' : 'OK: cifrato';
"

È lo stesso file che gira in produzione: l'unica differenza è il profilo dev (che aggiunge Mailpit) e i valori nel .env.


Documentazione

Documento Contenuto
README.md Deploy, configurazione, uso, riferimento
HANDOFF.md (questo file) Stato, decisioni, debito tecnico

La specifica funzionale originale (TerManager2_v2.md) è stata rimossa dal working tree: descriveva funzionalità poi cambiate — in particolare un wizard di primo avvio successivamente eliminato in favore della pagina Impostazioni — e conteneva checklist di sviluppo ormai esaurite. Le parti ancora valide (modello dati, regole di business, RBAC) sono confluite nel README.

Resta consultabile nella storia del repository:

git show 701f479:TerManager2_v2.md