Calendar Engine Esterno - Blueprint Tecnico¶
Obiettivo¶
Creare un motore calendario separato da SafeOps, riusabile da piu gestionali e da piu tenant, senza rompere l'attuale calendario interno.
Il motore deve diventare il punto centrale per:
- normalizzare appuntamenti provenienti da SafeOps e da altri gestionali;
- pubblicare calendari personali, tenant e multi-tenant;
- esporre feed ICS/WebCal compatibili con WebTop, Apple Calendar, Google Calendar e Outlook;
- preparare un connettore CalDAV per sincronizzazioni piu efficienti;
- mantenere audit, permessi e isolamento dati.
Principio Architetturale¶
SafeOps non deve perdere il controllo del dato operativo. Nella prima fase il motore esterno legge e normalizza eventi, ma la sorgente primaria resta SafeOps.
SafeOps
ticket_appointment
user_calendar_event
commerciale_richiesta_servizio.payload_json
formazione.data_corso
|
| adapter read-only
v
Calendar Engine
calendar_event
calendar_event_share
calendar_event_audit
calendar_feed_token
|
| ICS/WebCal
v
WebTop / Apple / Android / Outlook
Scope Release 1¶
Incluso:
- modello dati del motore;
- API interne read-only;
- import/adattatori da SafeOps;
- feed ICS per utente e tenant;
- token revocabili per feed;
- manuale operativo;
- integrazione WebTop tramite calendario Internet WebCal/ICS.
Fuori scope Release 1:
- modifica bidirezionale da WebTop verso SafeOps;
- CalDAV server completo;
- OAuth Google/Microsoft;
- app nativa iOS/Android;
- sostituzione delle viste calendario SafeOps esistenti.
Modello Dati Canonico¶
calendar_event¶
Evento normalizzato.
Campi minimi:
idinteger primary keytenant_keystring(80), not null, indexsource_systemstring(80), not nullsource_typestring(80), not nullsource_idstring(120), not nullsource_versionstring(80), nullableowner_user_idinteger, nullable, indexassigned_user_idinteger, nullable, indexcliente_idinteger, nullable, indextitlestring(255), not nulldescriptiontext, nullablelocationstring(255), nullablestarts_at_utcdatetime, not null, indexends_at_utcdatetime, not null, indextimezonestring(64), not null, defaultEurope/Romeall_dayboolean, not null, default falsestatusstring(40), not null, defaultconfirmedvisibilitystring(40), not null, defaultteammetadata_jsontext, nullablelast_source_hashstring(128), nullabledeleted_atdatetime, nullablecreated_atdatetime, not nullupdated_atdatetime, not null
Vincoli:
- unique
(tenant_key, source_system, source_type, source_id) - index
(tenant_key, starts_at_utc, ends_at_utc) - index
(tenant_key, owner_user_id, starts_at_utc) - index
(tenant_key, assigned_user_id, starts_at_utc) - index
(tenant_key, cliente_id, starts_at_utc) - index
(tenant_key, status)
calendar_event_share¶
Condivisione evento verso utenti specifici.
Campi:
idinteger primary keytenant_keystring(80), not null, indexevent_idinteger, not nulluser_idinteger, not null, indexpermissionstring(20), not null, defaultreadcreated_atdatetime, not null
Vincoli:
- unique
(event_id, user_id) - index
(tenant_key, user_id)
calendar_feed_token¶
Token revocabile per feed ICS/WebCal.
Campi:
idinteger primary keytenant_keystring(80), not null, indexuser_idinteger, nullable, indexscopestring(40), not nullsources_jsontext, nullabletoken_hashstring(128), not null, uniquelabelstring(120), nullableexpires_atdatetime, nullablerevoked_atdatetime, nullablecreated_by_user_idinteger, nullablecreated_atdatetime, not nulllast_used_atdatetime, nullable
Scope iniziali:
user: eventi visibili a un utente nel tenant;tenant: eventi di tutto il tenant, solo per admin;multi_tenant_user: eventi visibili allo stesso utente su piu tenant autorizzati;multi_tenant_admin: vista aggregata di tenant esplicitamente autorizzati.
calendar_event_audit¶
Audit append-only.
Campi:
idinteger primary keytenant_keystring(80), not null, indexevent_idinteger, nullable, indexactionstring(60), not nullactor_user_idinteger, nullablesource_systemstring(80), nullablepayload_jsontext, nullablecreated_atdatetime, not null
Azioni minime:
importedupdated_from_sourcedeleted_from_sourcefeed_createdfeed_revokedfeed_accessedpermission_denied
Sorgenti SafeOps Release 1¶
Ticket e interventi¶
Origine:
ticket_appointment- join con
ticket
Mapping:
source_system:safeopssource_type:ticket_appointmentsource_id: id appuntamento tickettenant_key:ticket_appointment.tenant_keyassigned_user_id:ticket_appointment.assigned_user_idoticket.assigned_user_idcliente_id:ticket.cliente_idstatus:programmato,completato,annullato
Eventi personali¶
Origine:
user_calendar_eventuser_calendar_event_share
Mapping:
source_type:user_calendar_eventsource_id: id evento personaleowner_user_id:user_calendar_event.user_idvisibility:team,private,selected
RAS e sopralluoghi¶
Origine:
commerciale_richiesta_servizio.payload_json["ras_appointment"]- eventuale
ticket_appointment_id
Mapping:
source_type:ras_appointmentsource_id: id richiesta serviziocliente_id:commerciale_richiesta_servizio.cliente_idmetadata_json:richiesta_id,workflow_type,stato,derived_from_compila, link SafeOps
Nota: in Release 1 il motore non corregge la fonte RAS. Si limita a leggere e normalizzare.
Formazione¶
Origine:
corsodata_corso
Mapping:
source_type:training_sessionsource_id: id data corso + slotcliente_id: nullmetadata_json:corso_id,data_corso_id,slot,aula
API Interne¶
PUT /api/v1/events¶
Upsert firmato di eventi normalizzati inviati da SafeOps o da un altro gestionale autorizzato.
Payload:
{
"items": [
{
"tenant_key": "tenant_demo",
"source_system": "safeops",
"source_type": "ticket_appointment",
"source_id": "123",
"title": "Ticket #123 - Sopralluogo",
"starts_at": "2026-08-01T09:00:00+02:00",
"ends_at": "2026-08-01T10:00:00+02:00",
"timezone": "Europe/Rome",
"visibility": "team",
"metadata": {"ticket_id": 123}
}
]
}
POST /api/v1/feed-tokens¶
Crea token revocabile per feed ICS/WebCal.
Scope Release 1:
usertenantmulti_tenant_usermulti_tenant_admin
POST /api/v1/feed-tokens/revoke¶
Revoca token feed tramite valore token originale.
GET /feed/<token>.ics¶
Feed ICS read-only consumabile da WebTop, Apple Calendar, Google Calendar, Outlook e client mobile.
Client SafeOps¶
Il client applicativo e in app.services.calendar_engine_client.
Responsabilita:
- leggere
CALENDAR_ENGINE_ENABLED,CALENDAR_ENGINE_BASE_URL,CALENDAR_ENGINE_SECRET; - leggere
CALENDAR_ENGINE_PUBLIC_BASE_URLper costruire URL feed pubblici dietro reverse proxy; - leggere
CALENDAR_ENGINE_WEBTOP_BASE_URL, se presente, per mostrare in SafeOps link ICS LAN compatibili con WebTop; - firmare ogni body con HMAC SHA-256;
- propagare
X-Request-IDquando presente; - gestire retry solo su errori temporanei;
- restituire
skipped=truequando il motore non e configurato.
Pagina Admin Parametri¶
SafeOps espone la vista CalendarEngineAdminView su:
/admin/calendar-engine/
Voce menu:
Configurazioni -> Calendar Engine
La vista e riservata ai platform admin e modifica solo parametri non segreti in .env.
Il segreto CALENDAR_ENGINE_SECRET e mostrato solo come stato configurato/non configurato.
Ogni salvataggio crea un backup .env.bak_calendar_engine_admin.
Parametri modificabili:
CALENDAR_ENGINE_ENABLED;CALENDAR_ENGINE_BASE_URL;CALENDAR_ENGINE_PUBLIC_BASE_URL;CALENDAR_ENGINE_WEBTOP_BASE_URL;CALENDAR_ENGINE_TIMEOUT_SECONDS;CALENDAR_ENGINE_FEED_PAST_DAYS;CALENDAR_ENGINE_FEED_FUTURE_DAYS;CALENDAR_ENGINE_FEED_MAX_EVENTS.
La pagina permette anche:
- creazione feed per tenant o utente;
- selezione sorgenti
ras_appointment,ticket_appointment,user_calendar_event; - elenco feed con stato attivo/revocato;
- revoca feed per ID interno firmato, senza conoscere il token originale;
- pulizia sicura dei feed gia revocati;
- diagnostica runtime del motore e dei feed;
- diagnostica sync schedulato Celery;
- ultima sync registrata con conteggi operativi.
Il token completo non viene mai salvato in chiaro ed e mostrato solo nella risposta di creazione.
La pulizia feed elimina solo righe con revoked_at valorizzato: i feed attivi non vengono rimossi in automatico.
Feed ICS:
X-WR-CALNAMEeX-WR-TIMEZONEvalorizzati per client WebCal/WebTop;- eventi con
UID,DTSTAMP,CREATED,LAST-MODIFIED,SEQUENCE,STATUS,TRANSPeCLASS; - header HTTP
ETag,Last-Modified,Cache-Control: private, max-age=300; - supporto
If-None-Matchcon risposta304 Not Modified.
Adapter SafeOps¶
Gli adapter applicativi sono in app.services.calendar_engine_adapters.
Mapping implementati:
ticket_appointment_to_calendar_eventuser_calendar_event_to_calendar_eventras_request_to_calendar_eventpublish_ras_request_appointment
Regola timezone:
- i datetime legacy senza timezone vengono interpretati come
Europe/Rome; - il payload inviato al motore usa timestamp ISO UTC con offset
+00:00; - il campo
timezoneresta valorizzato aEurope/Romeper compatibilita WebTop/mobile.
Primo publisher collegato:
- quando SafeOps crea o aggiorna un appuntamento RAS tramite
_upsert_ras_ticket_appointment, pubblica anche l'evento canonico verso Calendar Engine seCALENDAR_ENGINE_ENABLED=true.
Backfill e collaudo:
- servizio:
app.services.calendar_engine_sync - comando Flask:
flask --app app calendar_engine_sync - runner schedulato:
run_calendar_engine_scheduled_sync - task Celery:
app.tasks.calendar_engine_scheduled_sync - dry-run predefinito;
- pubblicazione effettiva solo con
--apply; - batch default: 100 eventi.
Esempio:
flask --app app calendar_engine_sync --tenant-key tenant_demo --source ras --limit 20
flask --app app calendar_engine_sync --tenant-key tenant_demo --source ras --limit 20 --apply
Sync schedulato:
- abilitazione:
CALENDAR_ENGINE_SCHEDULED_SYNC_ENABLED=true - schedule:
CALENDAR_ENGINE_SCHEDULED_SYNC_MINUTE=17,CALENDAR_ENGINE_SCHEDULED_SYNC_HOUR=* - tenant espliciti opzionali:
CALENDAR_ENGINE_SCHEDULED_SYNC_TENANTS=tenant_a,tenant_b - tenant automatici: assegnazioni attive in
user_role_profile_assignment, filtrate con modulocalendar; - fonti:
CALENDAR_ENGINE_SCHEDULED_SYNC_SOURCES=all - limiti:
CALENDAR_ENGINE_SCHEDULED_SYNC_LIMIT=500,CALENDAR_ENGINE_SCHEDULED_SYNC_BATCH_SIZE=100
La schedule e registrata in CELERY_BEAT_SCHEDULE e non dipende dal flag OPS_DAILY_CHECKS_ENABLED.
Ogni esecuzione di run_calendar_engine_scheduled_sync salva un riepilogo compatto nelle impostazioni operative
commerciale_tenant_setting, tenant tecnico __calendar_engine__, chiavi calendar_engine_sync:last_*.
Il riepilogo e usato dalla console admin per mostrare l'ultima sync senza leggere i log di sistema.
Comandi feed:
flask --app app calendar_engine_feed_create --tenant-key tenant_demo --scope user --user-id 7 --label "SafeOps - Mario Rossi"
flask --app app calendar_engine_feed_revoke --token <token-originale>
Unità di deploy:
deploy/systemd/safeops-calendar-engine.service- app Gunicorn:
services.calendar_engine.app:app - bind predefinito:
127.0.0.1:18092 - database SQLite predefinito unità :
/home/safeops/data/calendar_engine/calendar_engine.sqlite3
Reverse proxy:
- path consigliato:
/calendar-engine/ - upstream:
http://127.0.0.1:18092/ - template aggiornato:
deploy/nginx/safeops.conf.example - URL pubblico feed consigliato:
https://safeops.nxt-sense.eu/calendar-engine/feed/<token>.ics - finestra feed pilota consigliata:
CALENDAR_ENGINE_FEED_PAST_DAYS=365,CALENDAR_ENGINE_FEED_FUTURE_DAYS=180,CALENDAR_ENGINE_FEED_MAX_EVENTS=5000
Fallback:
- se il motore non e configurato, la chiamata viene saltata;
- se il motore risponde con errore temporaneo, SafeOps logga il problema ma non blocca il salvataggio operativo locale.
dry_run
Risposta:
- conteggio eventi letti;
- conteggio eventi creati;
- conteggio eventi aggiornati;
- conteggio eventi cancellati logicamente;
- warning.
GET /feed/<token>.ics¶
Feed ICS/WebCal.
La query feed usa una finestra temporale configurabile:
CALENDAR_ENGINE_FEED_PAST_DAYS, default30;CALENDAR_ENGINE_FEED_FUTURE_DAYS, default180;CALENDAR_ENGINE_FEED_MAX_EVENTS, default5000.
Regole:
- token salvato solo come hash;
- se token revocato o scaduto:
403; - range default: 30 giorni indietro, 180 giorni avanti;
- eventi privati inclusi solo se il token appartiene al proprietario.
Permessi Multi-Tenant¶
Il motore non deve fidarsi del solo tenant_key ricevuto dal client.
Ogni accesso deve verificare:
- identita utente o token feed;
- membership tenant;
- ruolo operativo;
- visibilita evento;
- sorgenti abilitate per tenant.
Per SafeOps, la sorgente membership iniziale e:
user_role_profile_assignmenttenant_registry- eventuali scope cliente gia presenti nel dominio SafeOps
Compatibilita Mobile e WebTop¶
Release 1 espone ICS/WebCal per compatibilita immediata.
NethServer 8 WebTop supporta calendari remoti WebCal/ICS e CalDAV. WebTop puo poi sincronizzare verso dispositivi mobili tramite ActiveSync, lasciando a WebTop la parte device-specific.
Percorso iniziale:
SafeOps -> Calendar Engine -> ICS/WebCal -> WebTop -> ActiveSync -> iOS/Android
Percorso evolutivo:
SafeOps <-> Calendar Engine <-> CalDAV <-> WebTop
Regole di Timezone¶
Persistenza:
- salvare sempre
starts_at_utceends_at_utc; - salvare anche
timezone, defaultEurope/Rome; - accettare input locale solo dagli adapter, convertendo prima della persistenza.
Output:
- ICS con timestamp UTC quando l'evento ha ora;
- all-day come data senza ora;
- UI con timezone dell'utente o del tenant.
Strategia di Migrazione¶
Fase 0 - Documentazione e contratto¶
- approvare questo blueprint;
- definire tenant pilota;
- definire utenti pilota;
- decidere host motore.
Fase 1 - Read-only¶
- creare schema motore;
- importare eventi SafeOps;
- esporre API eventi;
- esporre feed ICS;
- collegare WebTop come calendario remoto.
Fase 2 - UI SafeOps¶
- aggiungere flag
CALENDAR_ENGINE_ENABLED; - leggere il calendario globale dal motore per tenant pilota;
- mantenere fallback al calendario attuale.
Fase 3 - Scrittura Controllata¶
- nuove creazioni/spostamenti passano dal motore;
- il motore aggiorna SafeOps tramite adapter;
- audit obbligatorio su ogni mutazione.
Fase 4 - CalDAV¶
- esporre endpoint CalDAV;
- abilitare WebTop in modalita CalDAV;
- valutare sync differenziale e conflitti.
Fase 5 - Bidirezionale¶
- abilitare modifiche da WebTop solo su calendari autorizzati;
- gestire conflitti con
source_versionelast_source_hash; - loggare ogni cambio.
Criteri di Accettazione Release 1¶
- nessun evento di un tenant non autorizzato appare in feed o API;
- feed utente mostra solo eventi visibili all'utente;
- feed tenant richiede ruolo admin tenant;
- token revocato non funziona;
- evento RAS appare una sola volta anche se sincronizzato anche come ticket appointment;
- WebTop riesce a sottoscrivere il feed ICS;
- iPhone e Android vedono il calendario tramite WebTop/ActiveSync o iscrizione ICS diretta;
- il calendario SafeOps attuale continua a funzionare senza dipendere dal motore.
Riferimenti¶
- NethServer 8 WebTop groupware: https://docs.nethserver.org/projects/ns8/en/latest/webtop.html
- Documentazione utente WebTop su calendari remoti ICS/CalDAV: https://docs.nethserver.org/it/docs/user-manual/webtop