Vai al contenuto

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:

  • id integer primary key
  • tenant_key string(80), not null, index
  • source_system string(80), not null
  • source_type string(80), not null
  • source_id string(120), not null
  • source_version string(80), nullable
  • owner_user_id integer, nullable, index
  • assigned_user_id integer, nullable, index
  • cliente_id integer, nullable, index
  • title string(255), not null
  • description text, nullable
  • location string(255), nullable
  • starts_at_utc datetime, not null, index
  • ends_at_utc datetime, not null, index
  • timezone string(64), not null, default Europe/Rome
  • all_day boolean, not null, default false
  • status string(40), not null, default confirmed
  • visibility string(40), not null, default team
  • metadata_json text, nullable
  • last_source_hash string(128), nullable
  • deleted_at datetime, nullable
  • created_at datetime, not null
  • updated_at datetime, 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:

  • id integer primary key
  • tenant_key string(80), not null, index
  • event_id integer, not null
  • user_id integer, not null, index
  • permission string(20), not null, default read
  • created_at datetime, not null

Vincoli:

  • unique (event_id, user_id)
  • index (tenant_key, user_id)

calendar_feed_token

Token revocabile per feed ICS/WebCal.

Campi:

  • id integer primary key
  • tenant_key string(80), not null, index
  • user_id integer, nullable, index
  • scope string(40), not null
  • sources_json text, nullable
  • token_hash string(128), not null, unique
  • label string(120), nullable
  • expires_at datetime, nullable
  • revoked_at datetime, nullable
  • created_by_user_id integer, nullable
  • created_at datetime, not null
  • last_used_at datetime, 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:

  • id integer primary key
  • tenant_key string(80), not null, index
  • event_id integer, nullable, index
  • action string(60), not null
  • actor_user_id integer, nullable
  • source_system string(80), nullable
  • payload_json text, nullable
  • created_at datetime, not null

Azioni minime:

  • imported
  • updated_from_source
  • deleted_from_source
  • feed_created
  • feed_revoked
  • feed_accessed
  • permission_denied

Sorgenti SafeOps Release 1

Ticket e interventi

Origine:

  • ticket_appointment
  • join con ticket

Mapping:

  • source_system: safeops
  • source_type: ticket_appointment
  • source_id: id appuntamento ticket
  • tenant_key: ticket_appointment.tenant_key
  • assigned_user_id: ticket_appointment.assigned_user_id o ticket.assigned_user_id
  • cliente_id: ticket.cliente_id
  • status: programmato, completato, annullato

Eventi personali

Origine:

  • user_calendar_event
  • user_calendar_event_share

Mapping:

  • source_type: user_calendar_event
  • source_id: id evento personale
  • owner_user_id: user_calendar_event.user_id
  • visibility: team, private, selected

RAS e sopralluoghi

Origine:

  • commerciale_richiesta_servizio.payload_json["ras_appointment"]
  • eventuale ticket_appointment_id

Mapping:

  • source_type: ras_appointment
  • source_id: id richiesta servizio
  • cliente_id: commerciale_richiesta_servizio.cliente_id
  • metadata_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:

  • corso
  • data_corso

Mapping:

  • source_type: training_session
  • source_id: id data corso + slot
  • cliente_id: null
  • metadata_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:

  • user
  • tenant
  • multi_tenant_user
  • multi_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_URL per 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-ID quando presente;
  • gestire retry solo su errori temporanei;
  • restituire skipped=true quando 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-CALNAME e X-WR-TIMEZONE valorizzati per client WebCal/WebTop;
  • eventi con UID, DTSTAMP, CREATED, LAST-MODIFIED, SEQUENCE, STATUS, TRANSP e CLASS;
  • header HTTP ETag, Last-Modified, Cache-Control: private, max-age=300;
  • supporto If-None-Match con risposta 304 Not Modified.

Adapter SafeOps

Gli adapter applicativi sono in app.services.calendar_engine_adapters.

Mapping implementati:

  • ticket_appointment_to_calendar_event
  • user_calendar_event_to_calendar_event
  • ras_request_to_calendar_event
  • publish_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 timezone resta valorizzato a Europe/Rome per 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 se CALENDAR_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 modulo calendar;
  • 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, default 30;
  • CALENDAR_ENGINE_FEED_FUTURE_DAYS, default 180;
  • CALENDAR_ENGINE_FEED_MAX_EVENTS, default 5000.

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:

  1. identita utente o token feed;
  2. membership tenant;
  3. ruolo operativo;
  4. visibilita evento;
  5. sorgenti abilitate per tenant.

Per SafeOps, la sorgente membership iniziale e:

  • user_role_profile_assignment
  • tenant_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_utc e ends_at_utc;
  • salvare anche timezone, default Europe/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_version e last_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