# TerManager2 Applicazione web per la gestione dell'assegnazione e del rientro dei territori (cartoline) di congregazione, con statistiche di percorrenza, campagne speciali e registro storico completo. I dati personali dei proclamatori sono **cifrati nel database**, e ogni operazione è tracciata in un **audit log** consultabile. --- ## Indice **Per iniziare** - [Deploy su Dokploy](#deploy-su-dokploy) — la via consigliata per la produzione - [Deploy su altri sistemi](#deploy-su-altri-sistemi) — Coolify, Portainer, server con Docker - [Sviluppo locale](#sviluppo-locale) **Gestione quotidiana** - [Variabili d'ambiente](#variabili-dambiente) - [Impostazioni applicative](#impostazioni-applicative) - [Aggiornamenti](#aggiornamenti) - [Backup e ripristino](#backup-e-ripristino) - [Comandi utili](#comandi-utili) - [Troubleshooting](#troubleshooting) **Riferimento** - [Funzionalità](#funzionalità) - [Ruoli e permessi](#ruoli-e-permessi) - [Sicurezza e GDPR](#sicurezza-e-gdpr) - [Modello dati](#modello-dati) - [Regole di business](#regole-di-business) - [Architettura](#architettura) --- ## Deploy su Dokploy ### Come funziona L'immagine Docker è **autosufficiente**: dipendenze PHP e asset frontend sono compilati durante il build, non all'avvio. Nginx gira nello stesso container di php-fpm. Un deploy quindi è semplicemente: *build → migrazioni → online*. Non serve il codice sorgente sul server, non ci sono bind mount, e non viene eseguito né `composer install` né `npm build` all'avvio. | Servizio | Ruolo | Esposto | |----------------|------------------------------------------------|---------| | `app` | nginx + php-fpm — l'applicazione | via Traefik | | `queue-worker` | Job in coda (import PDF massivi) | no | | `scheduler` | Task pianificati (pulizia audit log) | no | | `mariadb` | Database | no | | `redis` | Cache, sessioni, code | no | Solo `app` è raggiungibile dall'esterno. Database e Redis restano su una rete interna senza rotta verso Internet. --- ### Passo 1 — Crea l'applicazione Su Dokploy: **Create Application** → tipo **Compose**. | Campo | Valore | |---------------|---------------------| | Repository | il tuo repo Git | | Branch | `main` | | Compose Path | `docker-compose.yml`| ### Passo 2 — Genera le credenziali Sul tuo computer, esegui questi comandi e tieni da parte l'output: ```bash echo "APP_KEY=base64:$(openssl rand -base64 32)" echo "DB_PASSWORD=$(openssl rand -base64 24)" echo "DB_ROOT_PASSWORD=$(openssl rand -base64 24)" echo "REDIS_PASSWORD=$(openssl rand -base64 24)" ``` > **Conserva `APP_KEY` in un password manager.** I dati personali dei > proclamatori sono cifrati con questa chiave: se la perdi, non sono più > leggibili nemmeno avendo il backup del database. ### Passo 3 — Imposta le variabili d'ambiente Nella sezione **Environment** di Dokploy, incolla questo blocco e sostituisci i valori: ```bash # --- Applicazione --- APP_NAME=TerManager2 APP_ENV=production APP_DEBUG=false APP_TIMEZONE=Europe/Rome # Deve combaciare con l'indirizzo da cui accedi, porta inclusa. # per IP:porta -> http://IP-DEL-SERVER:8080 # con dominio -> https://territori.tuodominio.it APP_URL=http://IP-DEL-SERVER:8080 # --- Chiavi e password (dal Passo 2) --- APP_KEY=base64:... DB_PASSWORD=... DB_ROOT_PASSWORD=... REDIS_PASSWORD=... # --- Database (mariadb e' il nome del servizio, non cambiarlo) --- DB_CONNECTION=mysql DB_HOST=mariadb DB_PORT=3306 DB_DATABASE=termanager2 DB_USERNAME=termanager2 # --- Redis (idem: redis e' il nome del servizio) --- REDIS_HOST=redis REDIS_PORT=6379 CACHE_STORE=redis SESSION_DRIVER=redis QUEUE_CONNECTION=redis # --- Primo amministratore (creato al primo avvio) --- INITIAL_ADMIN_NAME=Nome Cognome INITIAL_ADMIN_EMAIL=admin@tuodominio.it INITIAL_ADMIN_PASSWORD=UnaPasswordSolida # --- Porte esposte sull'host --- APP_BIND=0.0.0.0 # 127.0.0.1 se ci metti davanti un reverse proxy APP_PORT=8080 DB_BIND=127.0.0.1 # database raggiungibile solo via tunnel SSH DB_PORT_HOST=3306 # --- Sessioni e log --- # false con accesso HTTP, true solo in HTTPS: vedi .env.example SESSION_SECURE_COOKIE=false LOG_CHANNEL=stderr LOG_LEVEL=warning # --- Posta (SMTP esterno) --- MAIL_MAILER=smtp MAIL_HOST=smtp.tuoprovider.it MAIL_PORT=587 MAIL_USERNAME=utente MAIL_PASSWORD=password MAIL_SCHEME=tls MAIL_FROM_ADDRESS=noreply@tuodominio.it ``` > `APP_URL` deve corrispondere **esattamente** al dominio che servirai, schema > `https://` incluso. L'applicazione rifiuta richieste con un `Host` diverso: è > la protezione contro l'host header injection, ed è la causa più comune di > "pagina che non si apre" al primo deploy. ### Passo 4 — Assegna il dominio (facoltativo) Con la configurazione qui sopra l'applicazione risponde gia' su `http://IP-DEL-SERVER:8080`. Questo passo serve solo se vuoi un dominio con HTTPS — **consigliato**, perche' altrimenti password e sessioni viaggiano in chiaro. Quando lo fai, cambia anche: `APP_URL=https://tuodominio.it`, `SESSION_SECURE_COOKIE=true` e `APP_BIND=127.0.0.1`. **Domains** → **Add Domain**: | Campo | Valore | |----------------|-----------------------------| | Service Name | `app` | | Container Port | `80` | | Host | il tuo dominio | | HTTPS | attivo, Let's Encrypt | Dokploy genera da sé le label Traefik: non vanno scritte nel compose. ### Passo 5 — Deploy Premi **Deploy**. Il primo avvio esegue migrazioni, seed di ruoli e permessi, e crea l'amministratore. Richiede in genere uno o due minuti. Nei log dovresti vedere: ``` [✓] Storage directories ready. [✓] APP_KEY provided via environment. [✓] Database reachable. [✓] Migrations applied. [✓] Seed completed. Initial admin created: admin@tuodominio.it [✓] Caches warmed. TerManager2 — ready ``` Accedi con le credenziali `INITIAL_ADMIN_*` e **cambia subito la password**. ### Se qualcosa non parte Lo stack si rifiuta di avviarsi quando manca una variabile essenziale, con un messaggio esplicito: | Messaggio nei log | Cosa fare | |------------------------------------------------------|----------------------------------| | `required variable DB_PASSWORD is missing a value` | Imposta `DB_PASSWORD` | | `required variable REDIS_PASSWORD is missing a value` | Imposta `REDIS_PASSWORD` | | `[✗] APP_URL is not set` | Imposta `APP_URL` | | `[✗] APP_URL must include the scheme` | Aggiungi `https://` davanti | È voluto: meglio un errore immediato che un database in produzione con la password di default. --- ## Deploy su altri sistemi Lo stack è Docker Compose standard e funziona su **Coolify**, **Portainer**, **CapRover** o un semplice server con Docker. Le uniche due particolarità sono: 1. **Il file `.env`.** Dokploy lo genera dalle variabili della sua interfaccia. Altrove crealo a mano: `cp .env.example .env` e compila i valori. 2. **La rete `dokploy-network`.** In fondo a `docker-compose.yml` c'è una rete esterna che serve al Traefik di Dokploy per raggiungere il container. Hai due possibilità: - **Tenerla**, creandola una volta sola: `docker network create dokploy-network` - **Rimuoverla**: elimina `- dokploy-network` dalle reti del servizio `app` e il blocco `dokploy-network:` in fondo al file. Poi imposta nel `.env` la porta su cui esporre l'applicazione e avvia: ```bash # ascolta su tutte le interfacce, per metterci davanti un reverse proxy echo "APP_PORT=0.0.0.0:8080" >> .env docker compose up -d --build docker compose logs -f app ``` Infine metti il tuo reverse proxy (Nginx, Caddy, Traefik) davanti alla porta `8080`, con certificato TLS. --- ## Sviluppo locale Si usa lo stesso `docker-compose.yml` della produzione, con il profilo `dev` che aggiunge Mailpit: ```bash cp .env.example .env docker network create dokploy-network # una volta sola # nel .env, per l'uso locale: # APP_URL=http://localhost:8080 # APP_ENV=local # MAIL_MAILER=smtp MAIL_HOST=mailpit MAIL_PORT=1025 # e le tre password a piacere # (SESSION_SECURE_COOKIE=false e APP_BIND/APP_PORT vanno gia' bene) docker compose --profile dev up -d --build ``` | Cosa | Dove | |---------------------|-------------------------------| | Applicazione | http://localhost:8080 | | Mailpit (posta test)| http://localhost:8025 | | Login | quello che hai messo in `INITIAL_ADMIN_*` | Se la porta 8080 è occupata, cambia `APP_PORT` nel `.env` (es. `APP_PORT=127.0.0.1:8081`). ### Modifiche al codice senza ricostruire l'immagine L'immagine è autosufficiente: `vendor/` e `public/build` stanno dentro, quindi di norma una modifica al codice richiede `docker compose up -d --build`. Per avere invece le modifiche PHP immediate, crea un `docker-compose.override.yml` — Docker Compose lo applica da solo, senza bisogno di alcun flag: ```yaml services: app: environment: PHP_OPCACHE_VALIDATE_TIMESTAMPS: 1 volumes: - ./:/var/www/html # vendor e build restano quelli dell'immagine, non quelli dell'host - /var/www/html/vendor - /var/www/html/public/build ``` Il file è già in `.gitignore`, quindi resta solo tuo. Per lavorare sugli asset frontend con hot reload: ```bash npm install npm run dev ``` --- ## Variabili d'ambiente Il container legge la configurazione dall'ambiente di processo (su Dokploy: le variabili della UI, salvate in `.env`). Il file `docker-compose.yml` **non** ridichiara le variabili applicative: le prende dal `.env` così com'è. ### Obbligatorie | Variabile | Esempio | Note | |---------------------|-------------------------------|-----------------------------------------| | `APP_URL` | `https://territori.esempio.it`| Deve combaciare col dominio servito | | `APP_KEY` | `base64:...` | Cifra i dati personali — **da custodire**| | `DB_PASSWORD` | — | Nessun default | | `DB_ROOT_PASSWORD` | — | Nessun default | | `REDIS_PASSWORD` | — | Nessun default | ### Principali facoltative | Variabile | Default | Descrizione | |------------------------------|----------------|--------------------------------------| | `APP_NAME` | `TerManager2` | Nome mostrato nell'interfaccia | | `APP_ENV` | `production` | `local` in sviluppo | | `APP_DEBUG` | `false` | **Mai `true` in produzione** | | `APP_TIMEZONE` | `Europe/Rome` | Fuso orario | | `LOG_LEVEL` | `warning` | `debug` per diagnosticare | | `SESSION_SECURE_COOKIE` | `false` | `true` solo servendo in HTTPS | | `APP_BIND` | `0.0.0.0` | `127.0.0.1` dietro un reverse proxy | | `APP_PORT` | `8080` | Porta dell'applicazione sull'host | | `DB_BIND` | `127.0.0.1` | Interfaccia della porta MariaDB | | `DB_PORT_HOST` | `3306` | Cambiala se la 3306 e' occupata | | `INITIAL_ADMIN_EMAIL` | — | Admin creato al primo avvio | | `INITIAL_ADMIN_PASSWORD` | — | Idem | | `INITIAL_ADMIN_NAME` | `Amministratore`| Idem | | `MAIL_MAILER` | `log` | `smtp` per inviare davvero | | `RUN_MIGRATIONS` | `true` | Migrazioni automatiche all'avvio | | `RUN_DB_SEED_ON_FIRST_START` | `true` | Seed al primo avvio | ### Da non cambiare Questi valori sono **nomi di servizi** definiti in `docker-compose.yml`, non indirizzi di rete. Se li modifichi il container non trova più il database o Redis: ```dotenv DB_HOST=mariadb REDIS_HOST=redis ``` Nemmeno `CACHE_STORE`, `SESSION_DRIVER` e `QUEUE_CONNECTION` andrebbero spostati altrove: lo stack fornisce Redis e il queue worker consuma la coda Redis, quindi puntarli su un driver diverso interrompe l'elaborazione dei job. --- ## Impostazioni applicative Configurabili dall'interfaccia (**Impostazioni**, permesso `settings.manage`), non da variabili d'ambiente: | Impostazione | Descrizione | |--------------------------------|------------------------------------------------------| | Nome congregazione | Mostrato nell'intestazione e nei PDF | | Logo | Immagine nell'intestazione | | URL pubblico base | Base dei link PDF condivisibili | | Giorni giacenza "da assegnare" | Soglia oltre cui un territorio è da assegnare | | Mesi giacenza "prioritari" | Soglia oltre cui un territorio diventa prioritario | | Giorni per "smarrito" | Soglia oltre cui un'assegnazione è da rientrare | | Elementi nelle liste della home| Quanti record mostrare nelle tre liste rapide | | Validità link PDF (mesi) | Scadenza dei link pubblici alle cartoline | | Download PDF visibile | Mostra o nasconde il pulsante di download | | Retention audit log (giorni) | Dopo quanti giorni i log vengono cancellati | --- ## Aggiornamenti Push su `main` → **Deploy** su Dokploy. Ad ogni avvio il container: 1. ricrea le directory di storage e risolve `APP_KEY` 2. attende il database 3. applica le migrazioni pendenti 4. salta il seed (già eseguito — il marker è nel volume storage) 5. rigenera le cache di configurazione, rotte e viste Se le migrazioni falliscono il container **non parte**: preferibile a servire traffico su uno schema applicato a metà. Un aggiornamento ordinario non richiede alcun intervento manuale. --- ## Backup e ripristino ### Cosa salvare | Volume | Contenuto | Nel backup | |----------------|------------------------------------------------------|:----------:| | `db_data` | Database MariaDB | ✅ | | `storage_data` | PDF territori, miniature, log, `APP_KEY` di fallback | ✅ | | `redis_data` | Cache e sessioni — rigenerabili | — | Più il valore di **`APP_KEY`**. Il database da solo non basta: senza la chiave i dati personali cifrati non sono leggibili. ### Backup ```bash # Database docker exec mariadb-dump -u root -p"$DB_ROOT_PASSWORD" \ termanager2 | gzip > termanager2_$(date +%Y%m%d).sql.gz # File caricati (PDF e miniature) docker run --rm -v _storage_data:/data -v "$PWD":/backup \ alpine tar czf /backup/storage_$(date +%Y%m%d).tar.gz -C /data . ``` Automatizzabile con cron: ```bash 0 2 * * * docker exec mariadb-dump -u root -p"$DB_ROOT_PASSWORD" termanager2 | gzip > /backups/termanager2_$(date +\%Y\%m\%d).sql.gz ``` ### Ripristino ```bash gunzip -c termanager2_20260718.sql.gz | \ docker exec -i mariadb -u root -p"$DB_ROOT_PASSWORD" termanager2 docker exec php artisan migrate --force docker exec php artisan cache:clear ``` Assicurati che `APP_KEY` sia **la stessa** di quando il backup è stato creato. --- ## Comandi utili Sostituisci `` con il nome del container (lo trovi con `docker ps`). ```bash # Log in tempo reale docker logs -f # Stato delle migrazioni docker exec -it php artisan migrate:status # Shell interattiva Laravel docker exec -it php artisan tinker # Reimpostare la password di un utente docker exec -it php artisan tinker --execute=" \$u = App\Models\User::where('email','admin@tuodominio.it')->first(); \$u->password = Hash::make('NuovaPassword'); \$u->save(); " # Creare un altro amministratore docker exec -it php artisan app:create-initial-admin \ --name="Nome" --email="mail@dominio.it" --password="Password" # Pulizia manuale dell'audit log docker exec -it php artisan audit:cleanup # Svuotare le cache docker exec -it php artisan optimize:clear ``` --- ## Troubleshooting ### Il container `app` resta unhealthy L'health check interroga `/up`. Guarda i log (`docker logs `): quasi sempre è il database irraggiungibile o una migrazione fallita. ### Errore 400 o pagina bianca sul dominio `APP_URL` non corrisponde al dominio servito. Deve combaciare esattamente, schema `https://` incluso. È la protezione contro l'host header injection. ### La pagina appare senza stile (CSS mancante) Gli asset sono compilati dentro l'immagine. Se mancano, è fallito lo stage di build frontend: cerca `npm run build` nel log di build del deploy. ### I job restano in coda Verifica che `queue-worker` sia healthy e che `REDIS_PASSWORD` sia identica per tutti i servizi. ### L'import PDF non termina L'import ha un timeout di 15 minuti; oltre, il job viene marcato failed e i file temporanei rimossi. Controlla i log del worker per individuare il PDF responsabile. ### I dati cifrati non sono più leggibili `APP_KEY` è cambiata. Ripristina il valore originale: non esiste altro modo di recuperare i dati. Se la chiave è persa definitivamente, i proclamatori vanno reinseriti (territori, assegnazioni e storico restano intatti — non sono cifrati). ### Reset completo (solo sviluppo) ```bash docker compose --profile dev down -v docker compose --profile dev up -d --build ``` `-v` cancella i volumi: database, Redis e file caricati vengono persi. --- ## Funzionalità ### Dashboard - Anno teocratico corrente (creato automaticamente, settembre → agosto) - Media mensile di percorrenza e durata media delle assegnazioni - Campagna attiva con barra di progresso - Tre liste rapide con azione diretta: **da assegnare**, **prioritari**, **da rientrare** ### Territori - CRUD completo con upload PDF e miniatura generata automaticamente - Filtri per zona, tipologia e stato - Badge di stato colorati e indicatore di priorità (manuale o automatica) - Dettaglio con visualizzatore PDF e storico assegnazioni per anno teocratico - Link PDF pubblico condivisibile, a scadenza configurabile - Soft delete con cestino (ripristino o eliminazione definitiva) - Import massivo di PDF da cartella o archivio ZIP, elaborato in background ### Proclamatori - CRUD con nome e cognome **cifrati nel database** - Conteggio dei territori attualmente assegnati - **Anonimizzazione GDPR** irreversibile - Soft delete con cestino ### Assegnazione e rientro - Selezione territorio disponibile e proclamatore attivo - Anno teocratico rilevato automaticamente dalla data - Validazione: territorio non già assegnato, entrambi attivi - Rientro con calcolo automatico dei giorni - **Prompt campagna**: se il rientro ricade nel periodo di una campagna, viene chiesto se conteggiarlo ### Campagne - CRUD con date di inizio e fine - Stato automatico: attiva, futura o conclusa - Percentuale di percorrenza in tempo reale - Dettaglio territorio per territorio ### Registro assegnazioni - Filtri per anno teocratico, zona, tipologia, stato e ricerca testuale - Colonne: territorio, proclamatore, date, giorni, campagna ### Audit log - Tracciamento automatico su tutti i modelli - Eventi di flusso espliciti: assegnazione, rientro, login, logout - Filtri per utente e tipo evento, con diff prima/dopo - Pulizia automatica schedulata secondo la retention configurata ### XML Exchange - Conversione di dump SQL legacy in XML compatibile - Import ed export XML dell'intero set di dati gestionali ### Zone e tipologie - Gestione inline: aggiungi, rinomina, attiva/disattiva, elimina - Non eliminabili se hanno territori associati --- ## Ruoli e permessi Tre ruoli con permessi granulari (spatie/laravel-permission): | Permesso | Amministratore | Assistente | Operatore | |-----------------------|:--------------:|:----------:|:---------:| | `settings.manage` | ✅ | | | | `territori.manage` | ✅ | | | | `proclamatori.manage` | ✅ | ✅ | | | `campagne.manage` | ✅ | ✅ | | | `registro.view` | ✅ | ✅ | | | `registro.export` | ✅ | | | | `territori.assign` | ✅ | ✅ | ✅ | | `territori.return` | ✅ | ✅ | ✅ | | `audit.view` | ✅ | | | | `audit.export` | ✅ | | | L'amministratore ha un bypass completo (`Gate::before` in `AppServiceProvider`). Il menu laterale mostra solo le voci per cui l'utente ha i permessi. I permessi sono verificati sia sulle rotte sia sulle richieste Livewire, quindi una revoca ha effetto immediato senza bisogno di ricaricare la pagina. --- ## Sicurezza e GDPR ### Sicurezza - **Cifratura a riposo**: nome e cognome dei proclamatori cifrati con AES-256-CBC tramite `APP_KEY` - **Password**: hash bcrypt - **RBAC**: middleware `permission:` sulle rotte, riapplicato anche alle richieste Livewire - **CSRF**: token su tutti i form - **Rate limiting**: sul login - **Host header**: accettate solo richieste il cui `Host` corrisponde a `APP_URL` - **Scheme forwarding**: lo schema è deciso dal server, non dall'header del client, per non falsare la validazione dei link firmati - **Security headers**: `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy` - **Sessioni**: in Redis, cookie httpOnly e `Secure` in produzione - **Esecuzione PHP**: solo `index.php` è un entrypoint valido; ogni altro `.php` restituisce 404 ### GDPR - **Minimizzazione**: dei proclamatori si conservano solo nome, cognome e stato - **Cifratura a riposo**: i dati sono illeggibili anche con accesso diretto al DB - **Audit trail**: registro completo delle operazioni - **Diritto all'oblio**: funzione di anonimizzazione irreversibile - **Retention**: cancellazione automatica dei log oltre il periodo configurato - **No dati sensibili nei log**: l'audit registra gli identificativi, non i nomi in chiaro --- ## Modello dati | Tabella | Descrizione | |-------------------|--------------------------------------------------------------| | `users` | Utenti dell'applicazione | | `settings` | Configurazione singleton | | `zone` | Zone territoriali | | `tipologie` | Tipologie di territorio | | `proclamatori` | Assegnatari, con nome/cognome cifrati e soft delete | | `territori` | Cartoline con zona, tipologia, PDF, miniatura, soft delete | | `anni_teocratici` | Periodi settembre → agosto | | `campagne` | Periodi speciali con date e descrizione | | `assegnazioni` | Territorio ↔ proclamatore, con date, anno e campagna | | `activity_log` | Audit trail | **Vincoli e indici principali** - `territori.numero` e `anni_teocratici.label` — univoci - `assegnazioni(territorio_id, returned_at)` — indice composito, è la ricerca più frequente dell'applicazione - `assegnazioni(proclamatore_id, returned_at)`, `assegnazioni(assigned_at)` - `territori(attivo, deleted_at)`, `proclamatori(deleted_at)` - Foreign key con `RESTRICT` su territorio e proclamatore, a protezione dell'integrità dello storico --- ## Regole di business ### Anno teocratico Va da **settembre ad agosto**: da settembre in poi l'anno è `corrente/corrente+1`, prima è `corrente-1/corrente`. Viene creato automaticamente quando serve. L'assegnazione conta per l'anno determinato dalla **data di assegnazione**. ### Stato del territorio Calcolato dinamicamente, non memorizzato: | Stato | Condizione | |----------------|-----------------------------------------------------| | `in_reparto` | Attivo, nessuna assegnazione aperta | | `assegnato` | Ha un'assegnazione con `returned_at` nullo | | `da_rientrare` | Assegnato da più giorni della soglia configurata | | `inattivo` | Contrassegnato come non attivo | ### Priorità Un territorio è prioritario se lo è per **flag manuale** o perché non viene assegnato da più mesi della soglia configurata. La soglia automatica **vince sempre** sul flag: l'amministratore può aggiungere priorità, non toglierla quando la soglia è scattata. ### Percentuale di percorrenza delle campagne - **Denominatore**: tutti i territori attivi - **Numeratore**: le assegnazioni collegate alla campagna e contrassegnate come conteggiate al rientro Al rientro la campagna viene proposta se il territorio era fuori almeno in parte del periodo: assegnato durante la campagna, rientrato durante la campagna, oppure fuori per tutta la sua durata. Se sia stato davvero percorso lo decide l'operatore con la spunta. Se più campagne corrispondono viene proposta la più recente. I territori assegnati durante la campagna e non ancora rientrati compaiono nel dettaglio come "in corso". --- ## Architettura ### Stack | Componente | Tecnologia | |-------------------|--------------------------------------------| | Framework | Laravel 11 | | PHP | 8.3 (FPM) | | Interfaccia | Livewire 3.5 + TailwindCSS 4 + Alpine.js | | Database | MariaDB 11 | | Cache/Sessioni/Code| Redis 7 | | Web server | Nginx, nello stesso container di php-fpm | | RBAC | spatie/laravel-permission | | Audit trail | spatie/laravel-activitylog | | PDF | barryvdh/laravel-dompdf + poppler-utils | | Build asset | Vite 6 (in fase di build immagine) | ### Struttura del progetto ``` TerManager2/ ├── app/ │ ├── Console/Commands/ # audit:cleanup, app:create-initial-admin │ ├── Http/Controllers/ # PDF pubblici, import, login │ ├── Jobs/ # Import PDF massivo (in coda) │ ├── Livewire/ # Tutti i componenti di interfaccia │ ├── Models/ # 9 modelli Eloquent │ ├── Providers/ # AppServiceProvider │ └── Services/ # Miniature PDF, stato import ├── bootstrap/app.php # Middleware, proxy e host fidati ├── config/ # Configurazione Laravel ├── database/ │ ├── migrations/ │ └── seeders/ # Ruoli, permessi, dati di sviluppo ├── docker/ │ ├── nginx/default.conf # Vhost │ ├── php/ │ │ ├── Dockerfile # Build multi-stage │ │ ├── entrypoint.sh # Avvio: chiave, migrazioni, cache │ │ ├── queue-entrypoint.sh # Avvio worker e scheduler │ │ ├── php.ini │ │ └── www.conf │ └── supervisor/ # nginx + php-fpm nello stesso container ├── resources/ # CSS, JS, viste Blade ├── routes/web.php # Rotte con i relativi permessi ├── docker-compose.yml # Unico, produzione e sviluppo ├── .env.example # Tutta la configurazione ├── HANDOFF.md # Stato del progetto per chi subentra └── README.md ``` ### Perché nginx e php-fpm nello stesso container Con container separati, nginx avrebbe bisogno di una propria copia di `public/` e del volume storage solo per servire i file statici. Per un'applicazione di questa scala è complessità senza beneficio: un solo container significa un solo health check, un solo target per il proxy e nessun volume condiviso da tenere allineato. --- ## Licenza Proprietaria. Tutti i diritti riservati.