Architettura¶
Un'installazione = un luogo. Ogni copia del repository gestisce un solo luogo prenotabile, descritto nel
file .env (SPAZIO_*, COMUNE_*). Un Comune con più luoghi installa una copia per ciascuno, con il suo
indirizzo web, il suo database e il suo ufficio di riferimento. Niente multi-sede nel codice: meno
configurazione, meno errori, permessi più semplici.
Schema architetturale¶
flowchart LR
ENV[[".env<br/>SPAZIO_* · COMUNE_*"]]
subgraph Utenti
C([Cittadino])
U([Ufficio])
K([Custode])
AG([Agenti IA esterni<br/>Claude · IDE · orchestratori])
end
subgraph Stack["Docker Compose: un'installazione per luogo"]
APP["spazio-comune-app<br/>FastAPI · Jinja2 · Bootstrap Italia<br/>:8000"]
DB[("spazio-comune-db<br/>PostgreSQL 17 · volume pgdati<br/>prenotazioni · slot")]
INF["spazio-comune-inference<br/>IA: regole in italiano<br/>o LLM OpenAI-compatibile"]
MCP["spazio-comune-mcp<br/>server MCP · :8090/mcp"]
OLL[("spazio-comune-ollama<br/>LLM locale, opzionale")]
end
subgraph Esterni["Servizi esterni (produzione)"]
SPID["SPID / CIE"]
PAGO["pagoPA · MyPay"]
CAL["Outlook · eAgenda<br/>iCal · CSV"]
end
ENV -.-> APP
C & U & K -->|HTTPS| APP
AG -->|MCP| MCP
MCP -->|"API REST /api/v1"| APP
APP --> DB
APP -->|rete interna| INF
INF -.-> OLL
APP -.-> SPID & PAGO
APP --> CAL
| Container | Ruolo | Porta |
|---|---|---|
spazio-comune-app |
sito, back-office, custode, API REST; unico a scrivere il database | 8000 |
spazio-comune-db |
PostgreSQL 17: prenotazioni e slot (volume pgdati) |
solo rete interna |
spazio-comune-inference |
assistente IA (intenti e FAQ); non vede il database né i dati personali | solo rete interna |
spazio-comune-mcp |
server MCP per altri agenti; usa solo le API dell'app | 127.0.0.1:8090 |
spazio-comune-demo |
istanza con orologio fisso e dati fittizi (profilo demo) |
8001 |
spazio-comune-ollama, -ollama-pull |
LLM locale (profilo llm) |
solo rete interna |
spazio-comune-test |
suite pytest (profilo test) |
– |
Il database¶
Il database è PostgreSQL 17: il container spazio-comune-db (volume pgdati, solo rete interna) oppure un
servizio gestito su cloud qualificato ACN, indicato con DATABASE_URL. Lo scrive solo l'app.
Perché PostgreSQL: - è lo standard dei servizi gestiti sui cloud qualificati per la PA, con backup, cifratura, audit e ripristino a un istante preciso; - ha vincoli che rendono impossibili le sovrapposizioni; - supporta date con fuso orario; - permette a un Comune con più luoghi di tenerli tutti su un solo server, un database per luogo, con report complessivi.
erDiagram
PRENOTAZIONI {
text codice PK "numero di prenotazione, es. GDC-7KQ2-M4XP"
text attivita
timestamptz inizio
timestamptz fine
text stato "ATTESA_PAGAMENTO, CONFERMATA, UTILIZZATA, BLOCCO, ANNULLATA, SCADUTA"
text iuv "pagoPA"
text cognome
text nome
}
SLOT {
timestamptz inizio PK
timestamptz fine
text codice_prenotazione FK "NULL = libero"
}
SLOT |o--o| PRENOTAZIONI : "occupato da"
slotha una riga per ogni orario del calendario e, incodice_prenotazione, il numero della prenotazione che lo occupa. Annullamento e scadenza del pagamento lo riportano aNULLnella stessa transazione.- La vista
v_occupazioneunisce le due tabelle: per ogni slot stato, numero, attività, intestatario. È la stessa informazione della griglia Occupazione degli slot in/ufficioe dell'API/api/v1/ufficio/occupazione. - Doppia prenotazione impossibile per costruzione, con tre vincoli del database:
- indice univoco sull'inizio delle prenotazioni attive;
- vincolo di esclusione
EXCLUDE USING gist (tstzrange(inizio, fine) WITH &&): due prenotazioni attive non si possono sovrapporre nel tempo, nemmeno con durate diverse o scrivendo a mano in tabella; UNIQUEsuslot.codice_prenotazione.
Un test lancia 40 prenotazioni simultanee sullo stesso slot, su connessioni diverse: ne passa una, le altre 39
ricevono "orario già prenotato".
- Schemi. L'app usa lo schema public; l'istanza demo usa lo schema demo (ricreato a ogni avvio); ogni
test crea ed elimina uno schema temporaneo.
docker exec -it spazio-comune-db psql -U spazio -d spazio_comune -c \
"SELECT inizio, stato, codice_prenotazione, cognome FROM v_occupazione WHERE codice_prenotazione IS NOT NULL ORDER BY inizio"
Struttura del repository¶
spazio-comune/
├── .env.example configurazione commentata (sviluppo e produzione)
├── docker-compose.yml app, inference, mcp; profili demo, llm, test
├── zensical.toml questo sito
├── app/
│ ├── prenota/
│ │ ├── main.py rotte cittadino, ufficio, custode, API REST
│ │ ├── booking.py slot, prenotazioni, pagamenti, check-in
│ │ ├── catalog.py il luogo (da .env), attività, tariffe
│ │ ├── db.py PostgreSQL: prenotazioni, slot, v_occupazione, vincoli
│ │ ├── ai.py client inferenza + base di conoscenza FAQ
│ │ ├── adapters/ identity, payments, agenda
│ │ └── templates/ Jinja2 + Bootstrap Italia
│ ├── scripts/
│ │ ├── demo.py istanza con dati fittizi
│ │ ├── simula.py simulazione di prenotazioni concorrenti
│ │ └── registra_demo.py video MP4/GIF con Playwright
│ └── tests/
├── inference/ server IA: API OpenAI-compatibile + /v1/tasks/*
├── mcp_server/ server MCP (stdio o HTTP)
└── docs/ sorgenti del sito
Ciclo di vita della prenotazione¶
stateDiagram-v2
[*] --> ATTESA_PAGAMENTO: cittadino sceglie slot
ATTESA_PAGAMENTO --> CONFERMATA: esito pagoPA positivo / conferma sportello
ATTESA_PAGAMENTO --> SCADUTA: 15 minuti senza pagamento
SCADUTA --> CONFERMATA: pagamento tardivo con slot ancora libero
CONFERMATA --> UTILIZZATA: check-in custode (QR firmato HMAC)
CONFERMATA --> ANNULLATA: cittadino (fino a 24 h prima) o ufficio
[*] --> BLOCCO: riserva dell'ufficio
BLOCCO --> ANNULLATA
Scelte principali¶
- Un luogo per installazione. Configurazione nel
.env, nessuna logica multi-sede. - Pagamento idempotente.
verifica_pagamentosi può chiamare dal ritorno utente, da polling o da notifica. Se il pagamento arriva dopo che lo slot è stato preso da altri, viene segnalato il rimborso. - Render lato server. Funziona senza JavaScript: più accessibile e più robusto sui telefoni datati.
- Tariffe in un solo punto (
catalog.py). Una nuova delibera si recepisce in pochi minuti. - MCP come porta per gli agenti. Il server MCP usa le stesse API pubbliche del sito: nessun accesso diretto ai dati.
Dal prototipo alla produzione¶
| Componente | Prototipo | Produzione |
|---|---|---|
| SPID/CIE | MockIdentity |
gateway del portale eCOMUNE o proxy SPID/CIE OIDC certificato |
| pagoPA | MockPagoPA |
MyPaySILGateway: paaSILInviaDovuti / paaSILChiediPagati con credenziali Regione Puglia |
| Database | PostgreSQL 17 in container (spazio-comune-db) |
PostgreSQL gestito su cloud qualificato ACN (DATABASE_URL con sslmode=require), oppure il container con backup pg_dump cifrati |
| IA | motore a regole | invariato, oppure LLM locale dopo la valutazione d'impatto |
- Client SOAP MyPay SIL e job di verifica dei pagamenti in attesa
- Integrazione SPID/CIE
- Calendario reale concordato con l'Ufficio Sport
- Rate limiting, log di audit dell'ufficio, backup, HTTPS con
COOKIE_SECURE=true - Testi privacy validati dal RPD, dichiarazione di accessibilità aggiornata
- Valutazione d'impatto IA prima di attivare un LLM
- Repository pubblico e
publiccode.ymlcon l'URL definitivo