Francesco Picone 726af6d5c3 ++ Update gitignore
2026-10-21 17:55:24 +02:00
2026-10-21 17:55:24 +02:00
2026-10-21 17:55:24 +02:00
2026-10-19 17:11:30 +02:00
2026-04-05 19:26:04 +02:00
2026-07-30 17:05:39 +02:00

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), un modulo pubblico con cui i proclamatori chiedono un territorio o ne annunciano la riconsegna, l'elenco delle cartoline cartacee fuori, il modulo S-13 per email ogni mese 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

Gestione quotidiana

Riferimento


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 (pulizie, S-13 mensile) 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:

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:

# --- 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:

# 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:

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:

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:

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'è.

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:

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:

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

# 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:

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

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).

# 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

# Pulizia manuale delle richieste dal modulo pubblico gestite da più di 90 giorni
docker exec -it <app> php artisan richieste:pulizia

# Mandare subito il modulo S-13 agli amministratori (anno in corso, o un altro)
docker exec -it <app> php artisan registro:invia-s13
docker exec -it <app> php artisan registro:invia-s13 --anno=2025-2026

# Svuotare le cache
docker exec -it <app> php artisan optimize:clear

Le pulizie girano già da sole ogni notte e il modulo S-13 parte da solo l'ultima domenica del mese alle 8 (container scheduler); i comandi qui sopra servono solo per anticiparli. Senza un server di posta funzionante il modulo S-13 non parte (il comando lo dice).


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)

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
  • Richieste dal modulo pubblico (amministratore e assistenti): vedi Richieste dal modulo pubblico

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)
  • Cartolina cartacea: assegnando (anche il sorvegliante di gruppo) si può spuntare Ha chiesto la cartolina cartacea. Il segno sta sull'assegnazione: nell'elenco degli assegnati compare l'etichetta "cartaceo" e la pagina Cartacei (menu, sotto Assegnazioni) elenca i territori cartacei fuori e a chi, con i giorni e il pulsante Rientro. Al rientro escono dall'elenco

Richieste dal modulo pubblico

  • Modulo pubblico all'indirizzo /richiesta, senza login (l'indirizzo completo è scritto nella Home, da passare ai proclamatori): nome e cognome, nuovo territorio o riconsegna, una nota. Nella riconsegna il territorio si sceglie con il completamento automatico: si scrive il numero o la zona e compaiono i territori (attivi, o disattivati ma ancora fuori), con numero e zona soltanto; si accetta solo un territorio dell'elenco. A parte questo elenco il modulo non mostra nessun dato già presente: né nomi, né a chi è assegnato un territorio o se lo è
  • Contro lo spam: un campo trappola nascosto, un tempo minimo fra apertura e invio (in questi due casi risponde "inviata" senza salvare) e al massimo 5 richieste all'ora dallo stesso indirizzo
  • Avviso per email ad amministratori e assistenti (permesso richieste.gestisci) con l'email confermata, se il server di posta funziona; la richiesta resta comunque nella Home
  • Nella Home, riquadro Richieste: per un nuovo territorio i proclamatori attivi con un nome simile a quello scritto (uguale, iniziale, una lettera sbagliata, in qualsiasi ordine e senza accenti), ognuno porta al modulo Assegna già con quel proclamatore; Nessuno di questi apre il modulo vuoto. Per una riconsegna, a chi risulta assegnato il territorio e Registra rientro →. Fatta l'assegnazione o il rientro, la richiesta risulta evasa; altrimenti si può scartare
  • Nome e nota sono cifrati nel database e non vanno nel registro delle attività; le richieste gestite si cancellano da sole dopo 90 giorni (richieste:pulizia, ogni notte)

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 temporanea dalla pagina del gruppo o dal pannello Utenti ("Nuova password temporanea" sulla riga del sorvegliante): si vede una volta sola, va cambiata al primo accesso, e le sessioni aperte con quella vecchia si chiudono
  • 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. Toccando il numero di un territorio si apre l'anteprima (la miniatura della piantina) con il collegamento al PDF
  • 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
  • Modulo S-13 per email: l'ultima domenica di ogni mese alle 8 il modulo S-13 dell'anno di servizio in corso parte in PDF agli amministratori, all'email della loro utenza se confermata (registro:invia-s13, anche a mano). Non passa dalla coda: il PDF contiene i nomi dei proclamatori

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 ✅
richieste.gestisci ✅ ✅
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. Della sua utenza, in Utenti, si può solo generare una nuova password temporanea (e aprire il gruppo). 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 e sul modulo pubblico delle richieste (5 all'ora per indirizzo, più campo trappola e tempo minimo contro lo spam)
  • 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)
  • Richieste dal modulo pubblico: nome e nota cifrati come i dati dei proclamatori, fuori dal registro delle attività e dalla coda (l'email di avviso parte subito); cancellate 90 giorni dopo essere state gestite. Non sono nei backup (né XML né .zip, che contiene lo stesso XML): sono messaggi in arrivo, non dati gestionali, e un ripristino non le tocca. L'azzeramento (Zona pericolosa) le cancella
  • Email: il modulo S-13 (con i nomi) va solo agli amministratori con l'email confermata, l'avviso delle richieste solo a chi le gestisce
  • 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; cartaceo se il proclamatore ha chiesto la cartolina cartacea
richieste Richieste dal modulo pubblico: tipo (nuovo/riconsegna), nome e nota cifrati, numero del territorio, stato (aperta/evasa/scartata), l'assegnazione che l'ha evasa, chi e quando l'ha gestita
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.

Description
No description provided
Readme 4.2 MiB
Languages
PHP 64%
Blade 26.4%
CSS 4.2%
JavaScript 3.7%
Shell 0.9%
Other 0.8%