# 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 ### Dall'applicazione: pacchetto `.zip` (consigliato) *Backup e import* → **Crea e scarica backup**. Produce un unico `.zip` con tutti i dati gestionali, gli utenti e tutti i file caricati (PDF dei territori, miniature, logo). È la via da usare per spostare o rimettere in piedi un'installazione senza toccare i container. L'archivio si prepara in background e la pagina mostra l'avanzamento reale (file compressi / file totali); il download parte da solo appena è pronto. Stessa cosa per il ripristino: prima la barra di caricamento del pacchetto, poi quella delle fasi di lavoro (estrazione, database, file). Sulla stessa pagina, **Ripristina backup** ricarica il pacchetto: sostituisce i dati gestionali correnti *e* i file caricati. > **Dopo un ripristino tutti gli utenti — amministratore compreso — devono > impostare una nuova password.** Il pacchetto contiene gli hash delle password > di ogni account: chi lo ha avuto in mano ha potuto tentarne il crack offline, > quindi le vecchie credenziali non sono più considerate sicure. Al primo > accesso l'applicazione resta bloccata sulla schermata di cambio password. Il pacchetto **non** contiene `APP_KEY`: i nomi dei proclamatori sono cifrati con quella chiave, quindi il ripristino va fatto su un'installazione che ha la **stessa** `APP_KEY` di quando il backup è stato creato. Il tetto per l'upload è **256 MB** (`client_max_body_size` in `docker/nginx/default.conf` e `upload_max_filesize`/`post_max_size` in `docker/php/php.ini`). Oltre quella soglia usa il backup dei volumi qui sotto. ### Zona pericolosa: azzera tutto In fondo alla stessa pagina, **Scarica backup e azzera tutto** riporta l'applicazione allo stato di installazione appena eseguita. Va scritto `AZZERA TUTTO` per sbloccare il pulsante, e l'operazione procede in due tempi: 1. Viene creato e scaricato un backup completo di tutto ciò che c'è. 2. L'operazione **si ferma e aspetta**: controlli il file scaricato e confermi con un secondo clic. Fino a lì non viene toccato nulla, e puoi annullare. 3. Solo dopo la conferma vengono eliminati territori, proclamatori, assegnazioni, campagne, zone, tipologie, anni di servizio, il registro attività e tutti i file caricati; le impostazioni tornano ai default e i ruoli vengono riseminati. > **Tutti gli altri utenti vengono eliminati.** Resta solo l'account > dell'amministratore che ha eseguito l'operazione, con la sua password > attuale — come un'installazione nuova, che ha un amministratore solo. > Cancellarli tutti lascerebbe l'applicazione senza nessuno che possa > accedere fino al riavvio del container. Il backup scaricato al punto 1 è l'unico modo per tornare indietro. ### Dai volumi: 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 ### Backup e import - **Backup completo `.zip`**: dati gestionali, utenti e file caricati (PDF territori, miniature, logo) in un unico pacchetto scaricabile e ripristinabile - Barra di avanzamento reale su backup e ripristino (lavoro svolto, non tempo) - Ripristino da pacchetto, con cambio password obbligatorio per tutti gli utenti - **Zona pericolosa**: azzeramento completo con backup obbligatorio scaricato e confermato prima di procedere - Import ed export XML dell'intero set di dati gestionali (senza i file) - Conversione di dump SQL legacy in XML compatibile - Import massivo dei PDF dei territori da cartella o archivio ZIP ### 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 assegnati nel periodo della campagna - **Numeratore**: le assegnazioni contrassegnate come conteggiate --- ## 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.