session.upload_progress.enabled = 0 e session.use_strict_mode = 1, come indicato da Sansec in attesa della patch; restano anche dopo, perche' all'applicazione non servono (Laravel ha le sue sessioni, in Redis). Test di guardia in VersionePhpTest. HANDOFF: stato della mitigazione e scelta ancora aperta sul rilascio in produzione. README: sicurezza. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1074 lines
47 KiB
Markdown
1074 lines
47 KiB
Markdown
# TerManager2
|
|
|
|
Applicazione web per la gestione dell'assegnazione e del rientro dei territori
|
|
(cartoline) di congregazione, con statistiche di percorrenza, campagne speciali,
|
|
gruppi di servizio (un sorvegliante distribuisce ai membri un pacchetto di
|
|
territori) 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) — compreso come lanciare i [test automatici](#test-automatici)
|
|
|
|
**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
|
|
```
|
|
|
|
### Test automatici
|
|
|
|
La suite gira in un container dedicato: l'immagine di produzione installa le
|
|
dipendenze senza quelle di sviluppo, quindi PHPUnit lì dentro non c'è.
|
|
|
|
```bash
|
|
docker compose --profile test run --build --rm test
|
|
|
|
# una sola classe
|
|
docker compose --profile test run --build --rm test --filter BackupArchiveTest
|
|
|
|
# un solo gruppo
|
|
docker compose --profile test run --build --rm test --testsuite Unit
|
|
```
|
|
|
|
Il container esce col codice di PHPUnit, quindi la riga di comando si incatena
|
|
come qualsiasi altro comando.
|
|
|
|
**`--build` non è facoltativo.** Il sorgente non è montato: senza ricostruire,
|
|
si eseguono i test dell'ultima immagine costruita, che possono essere di due
|
|
modifiche fa. Ometterlo ha senso solo per rilanciare la stessa suite subito
|
|
dopo, quando si sa che nulla è cambiato.
|
|
|
|
Cosa serve sapere:
|
|
|
|
- **Non serve nient'altro acceso.** `phpunit.xml` impone SQLite in memoria,
|
|
cache `array` e code `sync`: la suite passa a stack spento e non tocca né il
|
|
database di sviluppo né il volume `storage_data`.
|
|
- **`APP_KEY` di test è fissa e scritta in `phpunit.xml`.** Serve perché nome e
|
|
cognome dei proclamatori sono cifrati: senza chiave nemmeno un test che non
|
|
parla di cifratura riuscirebbe a scrivere una riga. Non ha alcun rapporto con
|
|
la chiave di produzione.
|
|
- **SQLite non è MariaDB.** Resta un punto che usa SQL specifico di MySQL —
|
|
`CAST(... AS UNSIGNED)` nell'ordinamento del Registro — e che quindi va
|
|
provato sullo stack vero, non qui.
|
|
|
|
La logica JavaScript senza DOM (la ricerca nei campi di scelta,
|
|
`resources/js/scelta.js`) ha i suoi test in `tests/js/`, con il test runner di
|
|
Node:
|
|
|
|
```bash
|
|
npm run test:js
|
|
# oppure, senza Node installato:
|
|
docker run --rm -v "$PWD":/app -w /app node:20-alpine node --test tests/js/
|
|
```
|
|
|
|
---
|
|
|
|
## 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 |
|
|
| `MAX_UPLOAD_MB` | `2048` | Tetto dei file caricati: governa insieme nginx, PHP e la validazione |
|
|
| `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, nei PDF e nell'informativa |
|
|
| Soglia da assegnare (giorni) | Dopo quanti giorni dal rientro un territorio è "da assegnare" |
|
|
| Soglia priorità (giorni) | Dopo quanti giorni dal rientro un territorio diventa prioritario |
|
|
| Soglia rientro (giorni) | Oltre questa un'assegnazione è "da rientrare"; è anche la durata del link alla piantina |
|
|
| Limite liste Home | Quanti territori mostrare nelle due liste della Home |
|
|
| "Scarica PDF" nel viewer | Mostra o nasconde il pulsante di download a chi apre il link |
|
|
| Conservazione audit (giorni) | Dopo quanti giorni le voci del registro delle attività si cancellano |
|
|
|
|
Il server di posta ha una pagina sua (*Impostazioni → Server di posta*). Il
|
|
logo nell'intestazione, se c'è, arriva da un backup: la pagina non lo carica.
|
|
|
|
---
|
|
|
|
## Aggiornamenti
|
|
|
|
Push su `main` → **Deploy** su Dokploy. Se l'autodeploy non parte entro un
|
|
paio di minuti (succede: vedi HANDOFF, *Dove gira davvero*), il deploy si
|
|
avvia a mano dall'interfaccia o con `compose.deploy`. 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
|
|
|
|
### Com'è fatta la pagina *Backup e import*
|
|
|
|
Cinque zone numerate, in ordine di quanto sono definitive. L'indice in cima
|
|
salta su ciascuna evidenziandola:
|
|
|
|
| # | Zona | A cosa serve |
|
|
|---|------|--------------|
|
|
| 1 | Backup completo | Crea e scarica il pacchetto `.zip` |
|
|
| 2 | Ripristina da backup | Ricarica un pacchetto: **sostituisce** dati e file |
|
|
| 3 | Import PDF territori | Associa i PDF ai territori (da archivio ZIP o da cartella) |
|
|
| 4 | Dati in formato XML | Esporta/importa i soli dati, converte i dump della vecchia app |
|
|
| 5 | Zona pericolosa | Azzera l'installazione |
|
|
|
|
In fondo alla finestra c'è **una sola console**, comune a tutte le operazioni:
|
|
si apre da sé quando qualcosa parte, scorre da sola man mano che arrivano le
|
|
righe e riporta sia quello che succede nel browser (file scelto, avanzamento del
|
|
caricamento, codici HTTP, timeout) sia quello che succede sul server, in ordine
|
|
di tempo e con la sorgente accanto a ogni riga. Il pulsante **Copia** serve
|
|
proprio a questo: il primo passo di ogni diagnosi è farsi mandare quel testo.
|
|
|
|
Se torni indietro a leggere, lo scorrimento automatico si sospende e compare
|
|
*Torna in fondo*.
|
|
|
|
### 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 console 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,
|
|
accanto al pulsante, poi le fasi di lavoro nella console (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 lo decide **`MAX_UPLOAD_MB`** nel `.env` (default: 2048,
|
|
cioè 2 GB). Da quel valore l'entrypoint genera `client_max_body_size` per nginx
|
|
e `upload_max_filesize`/`post_max_size` per PHP, e la validazione lo rilegge:
|
|
un solo numero per tutta la catena.
|
|
|
|
> Se un caricamento si pianta a una percentuale bassa **senza messaggi**, è
|
|
> quasi sempre questo il limite. Superato il tetto nginx risponde `413` dopo
|
|
> aver accettato zero byte: il browser si vede chiudere la connessione a metà
|
|
> invio e la barra resta ferma. Dalla versione attuale la pagina controlla la
|
|
> dimensione *prima* di iniziare e lo dice subito, ma se aggiorni un'installazione
|
|
> vecchia il sintomo è quello.
|
|
|
|
Per alzarlo servono un `MAX_UPLOAD_MB` più grande **e** un redeploy (i limiti si
|
|
scrivono all'avvio del container). Alzarlo non costa memoria — nginx accumula il
|
|
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
|
|
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** in fondo alla finestra mostra il codice HTTP ricevuto: se
|
|
> leggi `413` con un pacchetto sotto `MAX_UPLOAD_MB`, il tetto è a monte.
|
|
|
|
### Zona pericolosa: azzera tutto
|
|
|
|
Zona 5, 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**, e la richiesta di conferma compare lì
|
|
nella zona 5, sotto il pulsante che l'ha avviata: controlli il file
|
|
scaricato, digiti la **tua password** e confermi con un secondo clic. Fino a
|
|
lì non viene toccato nulla, e puoi annullare. Dopo cinque password sbagliate
|
|
l'azzeramento resta bloccato per 5 minuti (l'accesso all'applicazione no).
|
|
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 <container-db> 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 <progetto>_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 <container-db> 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 <container-db> mariadb -u root -p"$DB_ROOT_PASSWORD" termanager2
|
|
|
|
docker exec <container-app> php artisan migrate --force
|
|
docker exec <container-app> php artisan cache:clear
|
|
```
|
|
|
|
Assicurati che `APP_KEY` sia **la stessa** di quando il backup è stato creato.
|
|
|
|
---
|
|
|
|
## Comandi utili
|
|
|
|
Sostituisci `<app>` con il nome del container (lo trovi con `docker ps`).
|
|
|
|
```bash
|
|
# Log in tempo reale
|
|
docker logs -f <app>
|
|
|
|
# Stato delle migrazioni
|
|
docker exec -it <app> php artisan migrate:status
|
|
|
|
# Shell interattiva Laravel
|
|
docker exec -it <app> php artisan tinker
|
|
|
|
# Reimpostare la password di un utente
|
|
docker exec -it <app> 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 <app> php artisan app:create-initial-admin \
|
|
--name="Nome" --email="mail@dominio.it" --password="Password"
|
|
|
|
# Pulizia manuale dell'audit log
|
|
docker exec -it <app> php artisan audit:cleanup
|
|
|
|
# Pulizia manuale degli archivi di backup residui (default: più vecchi di 24h)
|
|
docker exec -it <app> php artisan backup:cleanup
|
|
docker exec -it <app> php artisan backup:cleanup --ore=0 # tutti
|
|
|
|
# Svuotare le cache
|
|
docker exec -it <app> php artisan optimize:clear
|
|
```
|
|
|
|
Entrambe le pulizie girano già da sole ogni notte (container `scheduler`); i
|
|
comandi qui sopra servono solo per anticiparle.
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Il container `app` resta unhealthy
|
|
|
|
L'health check interroga `/up`. Guarda i log (`docker logs <app>`): 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
|
|
- Due liste rapide con azione diretta: **da assegnare** (prima i prioritari,
|
|
con il badge ★, poi quelli fermi da più tempo) e **da rientrare**; quando
|
|
l'elenco è troncato, **"Vedi tutti"** è un pulsante in testata che apre
|
|
l'elenco territori già filtrato. "Assegna →" apre la pagina *Assegna* con
|
|
il territorio scelto e senza l'elenco dei territori assegnati
|
|
|
|
### Territori
|
|
- CRUD completo con upload PDF e miniatura generata automaticamente
|
|
- **"Nuovo territorio"** apre il designer delle piantine già in disegno: il
|
|
territorio si crea dal confine disegnato (per chi non è amministratore resta
|
|
il form)
|
|
- Filtri per zona, tipologia e stato, conservati nell'indirizzo della pagina
|
|
- Badge di stato colorati e indicatore di priorità (manuale o automatica)
|
|
- Dettaglio con visualizzatore PDF e storico assegnazioni per anno teocratico:
|
|
una tabella sola con le colonne in linea, l'anno come riga di separazione,
|
|
dal più recente (lo stesso nella scheda del proclamatore)
|
|
- 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
|
|
- **Scelta con ricerca** di territorio e proclamatore (anche nel Registro e nel
|
|
designer delle piantine): si scrive e compaiono le proposte più vicine —
|
|
uguale, inizia con, contiene, errore di battitura ("forse cercavi") — con
|
|
le parole in qualsiasi ordine, senza badare a maiuscole e accenti. Campo
|
|
vuoto: l'elenco completo. Pensata anche per il telefono
|
|
- I territori già assegnati si trovano cercando ("già assegnato a …") ma non
|
|
si possono scegliere; l'anteprima della miniatura segue la scelta
|
|
- Anno teocratico rilevato automaticamente dalla data
|
|
- Validazione: territorio non già assegnato, entrambi attivi
|
|
- **Invio del link dopo l'assegnazione**: una finestra chiede il canale —
|
|
*Copia link*, oppure *Condividi…* (il menu di condivisione del telefono:
|
|
WhatsApp, Telegram, SMS…, dove il browser lo offre) — e se il link è stato
|
|
inviato. Il destinatario lo sceglie chi usa l'app dai propri contatti: i
|
|
proclamatori non hanno numero né email. Solo dopo la risposta si va avanti;
|
|
altrimenti l'assegnazione resta segnalata **"Link ancora da inviare"**, con
|
|
"Invia link" per riprendere. Anche dall'elenco dei territori assegnati si
|
|
può copiare o condividere il link
|
|
- L'elenco dei territori assegnati si carica subito dopo la pagina e si filtra
|
|
nel browser, senza attese; ogni territorio ha il pulsante **Rientro**, che
|
|
porta alla registrazione del rientro e poi torna all'elenco. Aperta da
|
|
"Assegna →" della Home, la pagina mostra solo il modulo, senza l'elenco
|
|
- Rientro con calcolo automatico dei giorni
|
|
- **Prompt campagna**: se il territorio era fuori durante una campagna viene
|
|
chiesto se è stato percorso per la campagna; oltre la soglia "da rientrare"
|
|
compare un avviso di tempo di percorrenza molto alto
|
|
- **Dopo una conferma o un annulla si torna alla pagina di partenza**, con i
|
|
suoi filtri, ricerca, ordinamento e pagina (aperta dal menu: come prima)
|
|
|
|
### Gruppi di servizio
|
|
- Li gestisce solo l'amministratore (menu *Gruppi di servizio*): nome,
|
|
**sorvegliante** (un proclamatore attivo), **membri** e **pacchetto** di
|
|
territori. Un proclamatore sta in un solo gruppo; il sorvegliante ne è
|
|
anche membro
|
|
- Creando il gruppo il sorvegliante riceve un'**utenza** con un'**email
|
|
fittizia** creata dall'applicazione: iniziale del nome e cognome sul
|
|
dominio `termanager.it` (Francesco Fontana → `ffontana@termanager.it`; se
|
|
è già presa, un numero in coda), e una password temporanea. Si vedono una
|
|
volta sola; la password va cambiata al primo accesso (la pagina dice
|
|
"Password temporanea", non "Ripristino da backup"). L'email non si
|
|
chiede, non riceve posta e non va confermata. Due gruppi non possono avere
|
|
lo stesso nome. Se perde la password, l'amministratore ne genera un'altra
|
|
dalla pagina del gruppo
|
|
- I territori del pacchetto si aggiungono in blocco tra quelli in reparto,
|
|
**in ordine di priorità** come "Da assegnare" nella Home (prima i
|
|
prioritari, poi quelli fermi da più tempo), con ricerca, filtri per zona,
|
|
tipologia e "solo prioritari", e "Seleziona tutti quelli mostrati".
|
|
Restano **"In gruppo …"**: non sono un'assegnazione (statistiche e medie non
|
|
cambiano), escono da "Da assegnare" e dalla pagina *Assegna*
|
|
- Il sorvegliante entra e vede **solo "Il mio gruppo"** e il proprio profilo
|
|
(ogni altra pagina risponde 403): dà i territori ai membri — o a sé — e ne
|
|
nasce un'**assegnazione normale**, che conta come le altre ed è segnata
|
|
"via gruppo …"; poi manda il link (copia o condividi), registra i rientri,
|
|
apre la piantina e rimanda in reparto quelli non ancora dati. Il suo
|
|
pacchetto è in ordine di priorità (prima i da dare, prioritari in testa,
|
|
poi quelli già dati) e si filtra per stato, zona, tipologia e "solo
|
|
prioritari"; sotto il nome del gruppo compaiono nome e cognome del
|
|
sorvegliante
|
|
- Al **rientro** il territorio torna in reparto ed esce dal pacchetto
|
|
- **Sciogliere il gruppo** toglie il ruolo al sorvegliante e **elimina il
|
|
gruppo**, che non resta nello storico: i territori solo nel pacchetto
|
|
tornano in reparto, i membri si liberano e l'**utenza del sorvegliante si
|
|
elimina** (anche una sessione aperta si chiude): torna un proclamatore come
|
|
gli altri. Le assegnazioni che aveva fatto passano a chi scioglie il
|
|
gruppo; nel registro delle attività le sue voci restano a nome
|
|
"Sorvegliante del gruppo …". I territori già dati restano ai membri come
|
|
assegnazioni qualsiasi, che del gruppo conservano solo il nome ("via
|
|
gruppo …"). Per ripartire si crea un gruppo nuovo
|
|
- **Un sorvegliante che non è più attivo scioglie il gruppo**, come togliergli
|
|
il ruolo: succede se lo si rende non attivo (dall'elenco o dalla modifica),
|
|
se lo si sposta nel cestino o lo si anonimizza. Prima di disattivarlo o
|
|
cestinarlo dall'elenco si chiede conferma; riattivarlo non ricostruisce il
|
|
gruppo. Un membro non attivo resta nel gruppo ma non riceve territori; nel
|
|
cestino esce dal gruppo
|
|
- **Chi fa parte di un gruppo è segnalato**: badge "Gruppo …" o
|
|
"Sorvegliante del gruppo …" nell'elenco dei proclamatori (con il filtro
|
|
*In un gruppo di servizio*) e in testa alla scheda; nella scelta del
|
|
proclamatore (Assegna, Registro) la nota "gruppo …" accanto al nome, che
|
|
resta comunque sceglibile
|
|
- **Dettagli del gruppo**: la scheda del proclamatore dice se è sorvegliante
|
|
(gruppo, membri, territori nel pacchetto e da dare, assegnazioni nate dal
|
|
gruppo, email per entrare, stato della password e ultimo accesso) o membro; il
|
|
sorvegliante ritrova gli stessi numeri nel suo
|
|
*Profilo*
|
|
|
|
### 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,
|
|
password dell'amministratore e conferma esplicita 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
|
|
|
|
Quattro ruoli con permessi granulari (spatie/laravel-permission):
|
|
|
|
| Permesso | Amministratore | Assistente | Operatore | Sorvegliante |
|
|
|-----------------------|:--------------:|:----------:|:---------:|:------------:|
|
|
| `settings.manage` | ✅ | | | |
|
|
| `territori.manage` | ✅ | | | |
|
|
| `proclamatori.manage` | ✅ | ✅ | | |
|
|
| `campagne.manage` | ✅ | ✅ | | |
|
|
| `registro.view` | ✅ | ✅ | | |
|
|
| `registro.export` | ✅ | | | |
|
|
| `territori.assign` | ✅ | ✅ | ✅ | |
|
|
| `territori.return` | ✅ | ✅ | ✅ | |
|
|
| `audit.view` | ✅ | | | |
|
|
| `audit.export` | ✅ | | | |
|
|
| `gruppo.gestisci` | | | | ✅ |
|
|
|
|
L'amministratore ha un bypass completo (`Gate::before` in `AppServiceProvider`).
|
|
Il menu laterale mostra solo le voci per cui l'utente ha i permessi.
|
|
|
|
Il ruolo **sorvegliante** non si assegna dalla pagina *Utenti*: nasce creando
|
|
un gruppo di servizio e sparisce sciogliendolo. Chi ha solo quel ruolo è
|
|
confinato da `LimitaSorvegliante` alla pagina del proprio gruppo, al rientro
|
|
dei territori del gruppo e al profilo; le sue azioni ricontrollano che
|
|
territorio e assegnazione siano del suo gruppo.
|
|
|
|
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
|
|
- **Versione di PHP fissata alla patch** (`ARG PHP_VERSION` nel Dockerfile,
|
|
oggi 8.3.36 per CVE-2026-109618/109619): una build o prende quella versione
|
|
o fallisce, mai una base vecchia rimasta in cache. Va alzata a ogni rilascio
|
|
di sicurezza del ramo 8.3
|
|
- **Sessioni native di PHP chiuse**: `session.upload_progress.enabled = 0` e
|
|
`session.use_strict_mode = 1` in `php.ini` (l'applicazione usa le sessioni
|
|
di Laravel; è la mitigazione indicata per le stesse CVE)
|
|
|
|
### GDPR
|
|
- **Informativa** (menu *Privacy*, solo amministratore): i valori che possono
|
|
cambiare — congregazione, conservazione del log, scadenza dei link,
|
|
download del PDF, server di posta, durata della sessione, indirizzo e HTTPS,
|
|
ruoli con i loro permessi, quanti dati sono conservati — si leggono al
|
|
momento da impostazioni e configurazione (`App\Livewire\Privacy`)
|
|
- **Minimizzazione**: dei proclamatori si conservano solo nome, cognome e
|
|
stato. Niente telefono né email: il link lo manda chi assegna, scegliendo il
|
|
destinatario dai propri contatti, e l'app non vede quei contatti
|
|
- **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 (anche per l'utenza di un sorvegliante di gruppo, che porta il nome
|
|
di un proclamatore)
|
|
- **Attenzione ai backup**: il pacchetto `.zip` e l'export XML contengono i dati
|
|
dei proclamatori **in chiaro** (l'export li legge decifrati). Vanno custoditi
|
|
in un luogo protetto e cancellati quando non servono
|
|
|
|
---
|
|
|
|
## Modello dati
|
|
|
|
| Tabella | Descrizione |
|
|
|-------------------|--------------------------------------------------------------|
|
|
| `users` | Utenti; `password_change_required` blocca l'accesso finché non si sceglie una password nuova (`password_temporanea` dice se il motivo è una password provvisoria data dall'amministratore invece di un ripristino); `proclamatore_id` lega l'utenza del sorvegliante (email fittizia) al suo proclamatore; sciolto il gruppo l'utenza si elimina |
|
|
| `settings` | Configurazione singleton |
|
|
| `zone` | Zone territoriali |
|
|
| `tipologie` | Tipologie di territorio |
|
|
| `proclamatori` | Assegnatari, con nome/cognome cifrati e soft delete (niente contatti); `gruppo_servizio_id` è il gruppo di cui fanno parte |
|
|
| `gruppi_servizio` | Gruppi di servizio attivi: nome (unico), sorvegliante. Sciolto, il gruppo si elimina |
|
|
| `territori` | Cartoline con zona, tipologia, PDF, miniatura, soft delete; `gruppo_servizio_id` se sono nel pacchetto di un gruppo |
|
|
| `anni_teocratici` | Periodi settembre → agosto |
|
|
| `campagne` | Periodi speciali con date e descrizione |
|
|
| `assegnazioni` | Territorio ↔ proclamatore, con date, anno e campagna; stato dell'invio del link (`link_sent`, `link_da_inviare`, `link_canale`); `gruppo_servizio_id` se data dal sorvegliante di un gruppo ancora attivo, `gruppo_nome` che resta anche dopo lo scioglimento |
|
|
| `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, fuori dai pacchetti dei gruppi |
|
|
| `in_gruppo` | Nel pacchetto di un gruppo di servizio, non ancora dato a un membro |
|
|
| `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é è fermo
|
|
(dall'ultimo rientro, o dalla creazione se mai assegnato) da più giorni della
|
|
soglia configurata. La soglia automatica **vince sempre** sul flag:
|
|
l'amministratore può aggiungere priorità, non toglierla quando la soglia è
|
|
scattata. Assegnare il territorio azzera il flag.
|
|
|
|
Dove si sceglie un territorio da dare — "Da assegnare" nella Home, l'aggiunta
|
|
al pacchetto di un gruppo, "Il mio gruppo" — l'ordine è lo stesso: prima i
|
|
prioritari, poi quelli fermi da più tempo, poi il numero
|
|
(`Territorio::ordinaPerPriorita()`).
|
|
|
|
### Percentuale di percorrenza delle campagne
|
|
- **Denominatore**: tutti i territori attivi
|
|
- **Numeratore**: i territori attivi distinti con almeno un'assegnazione
|
|
collegata alla campagna e contrassegnata come conteggiata al rientro (un
|
|
territorio percorso due volte conta una volta sola)
|
|
|
|
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".
|
|
|
|
### Gruppi di servizio e statistiche
|
|
Mettere un territorio nel pacchetto di un gruppo **non** è un'assegnazione:
|
|
non crea righe in `assegnazioni`, quindi non tocca medie, durate, percorrenza
|
|
delle campagne né il registro. Conta solo quando il sorvegliante lo dà a un
|
|
membro: da lì è un'assegnazione come le altre (con `gruppo_servizio_id` per
|
|
sapere da dove viene). Il tempo passato nel pacchetto prima di essere dato non
|
|
viene contato. La priorità automatica guarda l'ultima assegnazione, come
|
|
sempre.
|
|
|
|
---
|
|
|
|
## Architettura
|
|
|
|
### Stack
|
|
|
|
| Componente | Tecnologia |
|
|
|-------------------|--------------------------------------------|
|
|
| Framework | Laravel 11 |
|
|
| PHP | 8.3 (FPM), versione fissata nel Dockerfile |
|
|
| 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, backup:cleanup, app:create-initial-admin,
|
|
│ │ # utenti:conferma-email, territori:file-orfani
|
|
│ ├── Http/Controllers/ # PDF pubblici, import, backup, login, cambio password
|
|
│ ├── Http/Middleware/ # RequirePasswordChange, LimitaSorvegliante
|
|
│ ├── Jobs/ # Import PDF, backup, ripristino, azzeramento (in coda)
|
|
│ ├── Livewire/ # Tutti i componenti di interfaccia (Gruppi/: gruppi di servizio)
|
|
│ ├── Models/ # 12 modelli Eloquent
|
|
│ ├── Providers/ # AppServiceProvider
|
|
│ └── Services/ # Miniature PDF, import, archivio backup, posta,
|
|
│ # piantine, modulo S-13, gruppi di servizio
|
|
├── 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 (runtime = ultimo 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/app.css # Sistema di pulsanti, tabelle responsive, console
|
|
│ ├── js/
|
|
│ └── views/
|
|
│ ├── components/btn.blade.php # L'unico pulsante dell'applicazione
|
|
│ └── livewire/ # Una vista per componente
|
|
├── routes/web.php # Rotte con i relativi permessi
|
|
├── tests/ # Suite PHPUnit (profilo compose "test") e tests/js (Node)
|
|
├── phpunit.xml # Configurazione e ambiente dei test
|
|
├── 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.
|