Compare commits

...

4 Commits

Author SHA1 Message Date
root
0a4317c44a Update README.md with comprehensive documentation of Advoware Calendar Sync implementation, including API quirks, token security, and known issues 2025-10-22 23:36:21 +00:00
root
c897f4c39d Fix: Logger-Aufrufe durch print statements ersetzt
- Alle context.logger Aufrufe durch print() ersetzt
- Behebt 'Logger object has no attribute' Fehler
- Temporäre Lösung bis Logger-API geklärt ist
2025-10-22 19:08:00 +00:00
root
805a1cce3e Fix: context Parameter für get_google_service Funktion
- context als Parameter an get_google_service() übergeben
- Behebt 'name context is not defined' Fehler
2025-10-22 19:06:07 +00:00
root
76a236ac37 Google Calendar Sync graceful handling für fehlende Credentials
- Graceful Fallback wenn token.pickle nicht vorhanden
- Warning statt Exception bei fehlenden Google Credentials
- Sync wird übersprungen statt zu crashen
2025-10-22 19:05:16 +00:00
3 changed files with 177 additions and 65 deletions

View File

@@ -13,35 +13,69 @@ Das System synchronisiert Termine zwischen:
### Automatische Kalender-Erstellung
- Für jeden Advoware-Mitarbeiter wird ein Google Calendar mit dem Namen `AW-{Kuerzel}` erstellt
- Beispiel: Mitarbeiter mit Kürzel "SB" → Calendar "AW-SB"
- Kalender wird mit dem Haupt-Google-Account (`lehmannundpartner@gmail.com`) als Owner geteilt
### Bidirektionale Synchronisation
### Bidirektionale Synchronisation mit Token-Validierung
#### Advoware → Google Calendar
- Alle Termine eines Mitarbeiters werden aus Advoware abgerufen
- Alle Termine eines Mitarbeiters werden aus Advoware abgerufen (Zeitraum: aktuelles Jahr + 2 Jahre)
- Neue Termine werden in den entsprechenden Google Calendar eingetragen
- Die Advoware-Termin-ID (`frNr`) wird als Metadaten gespeichert
- Bestehende Termine werden aktualisiert, wenn Änderungen erkannt werden
#### Google Calendar → Advoware
- Termine aus Google Calendar ohne `frNr` werden als neue Termine in Advoware erstellt
- Die generierte `frNr` wird zurück in den Google Calendar geschrieben
- Token-basierte Validierung verhindert unbefugte Änderungen
- Sync-Info wird in der Description/Notiz gespeichert
### Konfigurationsoptionen
- **Vollständige Termindetails**: Titel, Beschreibung, Ort werden synchronisiert
- **Nur "Blocked"**: Termine werden nur als "beschäftigt" markiert (keine Details)
### Token-Sicherheit
- MD5-Hash mit Salt (aus Umgebungsvariable `CALENDAR_SYNC_SALT`) für Änderungsvalidierung
- Sync-Info Format: `## no change below this line ##\nfrNr: {frNr}\nsync-token: {token}`
- Token wird bei jeder Änderung neu berechnet und validiert
### Löschungen
- Google-initiale Termine, die in Google gelöscht werden, werden auch in Advoware gelöscht
- Tracking von gelöschten `frNr` um Re-Sync zu verhindern
## API-Endpunkte
### Advoware
- `GET /api/v1/advonet/Mitarbeiter` - Liste aller Mitarbeiter
### Advoware API
- `GET /api/v1/advonet/Mitarbeiter?aktiv=true` - Liste aktiver Mitarbeiter
- `GET /api/v1/advonet/Termine?frnr={frnr}` - Einzelner Termin
- `GET /api/v1/advonet/Termine?kuerzel={kuerzel}&from={date}&to={date}` - Termine eines Mitarbeiters
- `POST /api/v1/advonet/Termine` - Neuen Termin erstellen
- `PUT /api/v1/advonet/Termine` - Termin aktualisieren
- `DELETE /api/v1/advonet/Termine?frnr={frnr}` - Termin löschen
**API-Schwächen und Seltsamkeiten:**
- Parameter müssen als Strings übergeben werden (z.B. `aktiv='true'`, nicht `True`)
- Response kann manchmal HTML statt JSON zurückgeben, auch bei 200 Status
- Content-Type Header ist nicht immer korrekt gesetzt
- Token-Authentifizierung mit HMAC-Signature erforderlich
- Keine Paginierung für große Resultate
- Zeitformate: `datum` als `YYYY-MM-DDTHH:MM:SS`, aber manchmal ohne T
### Google Calendar API
- Kalender-Management (erstellen, auflisten)
- Kalender-Management (erstellen, auflisten, ACL setzen)
- Event-Management (erstellen, aktualisieren, löschen)
- Service Account Authentifizierung mit Backoff bei Rate Limits
## Step-Konfiguration
### advoware_calendar_sync_step.py
### calendar_sync_event_step.py
- **Type:** event
- **Subscribes:** calendar.sync.triggered
- **Flows:** advoware
**Event Data:**
```json
{
"full_content": true // oder false für nur "blocked"
}
```
### calendar_sync_api_step.py
- **Type:** api
- **Path:** `/advoware/calendar/sync`
- **Method:** POST
@@ -54,17 +88,48 @@ Das System synchronisiert Termine zwischen:
}
```
### calendar_sync_cron_step.py
- **Type:** cron
- **Schedule:** Täglich um 2:00 Uhr
- **Flows:** advoware
## Setup
### Google API Credentials
1. Google Cloud Console Projekt erstellen
2. Google Calendar API aktivieren
3. OAuth 2.0 Credentials erstellen
4. `token.pickle` Datei im Projektverzeichnis bereitstellen
3. Service Account erstellen (kein OAuth)
4. `service-account.json` Datei im Projektverzeichnis bereitstellen
### Advoware API Credentials
OAuth-ähnliche Authentifizierung mit HMAC-Signature.
### Umgebungsvariablen
```env
GOOGLE_CALENDAR_CREDENTIALS_PATH=token.pickle
# Google Calendar
GOOGLE_CALENDAR_SERVICE_ACCOUNT_PATH=service-account.json
# Advoware API
ADVOWARE_API_BASE_URL=https://www2.advo-net.net:90/
ADVOWARE_PRODUCT_ID=64
ADVOWARE_APP_ID=your_app_id
ADVOWARE_API_KEY=your_api_key
ADVOWARE_KANZLEI=your_kanzlei
ADVOWARE_DATABASE=your_database
ADVOWARE_USER=your_user
ADVOWARE_ROLE=2
ADVOWARE_PASSWORD=your_password
ADVOWARE_TOKEN_LIFETIME_MINUTES=55
ADVOWARE_API_TIMEOUT_SECONDS=30
# Redis für Token Caching
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB_ADVOWARE_CACHE=1
REDIS_TIMEOUT_SECONDS=5
# Calendar Sync
CALENDAR_SYNC_SALT=your_secret_salt
```
## Verwendung
@@ -76,28 +141,35 @@ curl -X POST "http://localhost:3000/advoware/calendar/sync" \
-d '{"full_content": true}'
```
### Event-basierter Sync
```bash
curl -X POST "http://localhost:3000/events" \
-H "Content-Type: application/json" \
-d '{"type": "calendar.sync.triggered", "data": {"full_content": true}}'
```
### Automatischer Sync
Der Step kann über Cron-Jobs oder Events regelmäßig ausgeführt werden.
Der Cron-Step führt täglich um 2:00 Uhr einen vollständigen Sync aus.
## Datenmapping
### Advoware → Google Calendar
```javascript
{
"summary": appointment.text || "Advoware Termin",
"description": `Advoware Termin\nNotiz: ${appointment.notiz}\nOrt: ${appointment.ort}`,
"location": appointment.ort,
"summary": "Advoware blocked", // Immer "blocked" für Privacy
"description": `Advoware Termin ID: ${appointment.frNr}`,
"location": "",
"start": {
"dateTime": `${appointment.datum}T${appointment.uhrzeitBis || '00:00:00'}`,
"dateTime": start_datetime, // Berechnet aus datum/uhrzeitVon
"timeZone": "Europe/Berlin"
},
"end": {
"dateTime": `${appointment.datumBis || appointment.datum}T${appointment.uhrzeitBis || '23:59:59'}`,
"dateTime": end_datetime, // Berechnet aus datumBis/uhrzeitBis
"timeZone": "Europe/Berlin"
},
"extendedProperties": {
"private": {
"advoware_frnr": appointment.frNr
"advoware_frnr": appointment.frNr.toString()
}
}
}
@@ -106,56 +178,84 @@ Der Step kann über Cron-Jobs oder Events regelmäßig ausgeführt werden.
### Google Calendar → Advoware
```javascript
{
"frNr": int(frnr),
"text": event.summary,
"notiz": event.description,
"notiz": updated_description, // Mit sync-info
"ort": event.location,
"datum": event.start.dateTime.substring(0, 10),
"uhrzeitBis": event.start.dateTime.substring(11, 19),
"datumBis": event.end.dateTime.substring(0, 10),
"datum": start.split('+')[0] if '+' in start else start,
"uhrzeitBis": end.split('T')[1].split('+')[0] if 'T' in end else '09:00:00',
"datumBis": end.split('+')[0] if '+' in end else end,
"sb": employee_kuerzel,
"anwalt": employee_kuerzel
"anwalt": employee_kuerzel,
"vorbereitungsDauer": "00:00:00"
}
```
## Fehlerbehandlung
- **Google API Fehler:** Automatische Token-Refresh, Fallback bei Authentifizierungsfehlern
- **Advoware API Fehler:** Retry-Logic, detaillierte Logging
- **Netzwerkfehler:** Timeout-Handling, Wiederholungsversuche
- **Dateninkonsistenzen:** Validierung vor Sync, Konfliktlösung
- **Google API Fehler:** Exponentieller Backoff bei 403/429, automatische Wiederholung
- **Advoware API Fehler:** Token-Refresh bei 401, detaillierte Logging
- **JSON Parse Fehler:** Response-Text logging für Debugging
- **Netzwerkfehler:** Timeout-Handling, konfigurierbare Timeouts
- **Dateninkonsistenzen:** Token-Validierung, Änderungsvergleich vor Update
## Monitoring
### Logs
- Erfolgreiche Synchronisationen
- Fehlerhafte Termine
- Kalender-Erstellungen
- Performance-Metriken
- Erfolgreiche Synchronisationen mit Anzahl
- Fehlerhafte API-Calls mit Details
- Kalender-Erstellungen und ACL-Setups
- Performance-Metriken (Sync-Dauer)
- Debug-Logs für Änderungsvergleiche
### Metriken
- Anzahl synchronisierter Termine
- Verarbeitete Mitarbeiter
- Fehlerquoten
- Sync-Dauer
- Fehlerquoten pro API
- Gelöschte Termine
- Token-Validierungsfehler
## Sicherheit
- OAuth 2.0 für Google API Zugriff
- Token-Speicherung in verschlüsselten Dateien
- Scoped API Permissions (nur Calendar-Zugriff)
- Service Account für Google API (kein User-Consent)
- HMAC-SHA512 für Advoware Token-Generierung
- Token-Caching in Redis mit TTL
- MD5-Token für Sync-Validierung mit Salt
- Scoped Permissions (nur Calendar-Zugriff)
- Audit-Logs für alle Änderungen
## Bekannte Probleme und Workarounds
### Advoware API
- **Parameter-Typen:** Alle Query-Parameter müssen Strings sein (z.B. `'true'` statt `True`)
- **Response-Formate:** Manchmal HTML statt JSON, auch bei 200 Status
- **Content-Type:** Nicht immer korrekt gesetzt, führt zu Parse-Fehlern
- **Zeitformate:** Inkonsistente Formate, manuelle Parsing erforderlich
- **Rate Limits:** Keine dokumentierten Limits, aber praktische Limits vorhanden
### Google Calendar API
- **Rate Limits:** 403/429 mit Backoff-Handling
- **ACL-Sharing:** Owner-Rolle für Hauptaccount erforderlich
- **Event-Updates:** Vollständige Event-Daten bei Updates senden
### Sync-Logik
- **Token-Mismatch:** Verhindert unbefugte Änderungen
- **Deleted Tracking:** Verhindert Re-Sync gelöschter Termine
- **Änderungsvergleich:** Nur tatsächliche Änderungen syncen
## Erweiterungen
### Geplante Features
- Inkrementelle Syncs (nur geänderte Termine)
- Inkrementelle Syncs (nur geänderte Termine seit letztem Sync)
- Konfliktlösungsstrategien (Advoware gewinnt, Google gewinnt, Manuell)
- Batch-Verarbeitung für Performance
- Batch-Verarbeitung für bessere Performance
- Webhook-Integration für Echtzeit-Syncs
- Mehrere Google-Accounts unterstützen
- Outlook/Apple Calendar Integration
### Integration mit anderen Systemen
- Outlook Calendar
- Apple Calendar
- Mobile Apps
- Notification-Systeme
### Code-Verbesserungen
- Unit-Tests für API-Calls
- Mock-Server für Testing
- Config-Validation beim Startup
- Health-Checks für beide APIs
- Metrics-Export (Prometheus)

View File

@@ -22,7 +22,7 @@ config = {
SCOPES = ['https://www.googleapis.com/auth/calendar']
async def get_google_service():
async def get_google_service(context):
"""Initialisiert Google Calendar API Service"""
creds = None
@@ -38,7 +38,8 @@ async def get_google_service():
else:
# Hier würde normalerweise der OAuth Flow laufen
# Für Server-Umgebung brauchen wir Service Account oder gespeicherte Credentials
raise Exception("Google OAuth Credentials nicht gefunden. Bitte token.pickle bereitstellen.")
print("WARNING: Google OAuth Credentials nicht gefunden. Bitte token.pickle bereitstellen oder Google Calendar Sync überspringen.")
return None
# Token speichern
with open('token.pickle', 'wb') as token:
@@ -52,10 +53,10 @@ async def get_advoware_employees(context):
try:
# Annahme: Mitarbeiter-Endpoint existiert ähnlich wie andere
result = await advoware.api_call('Mitarbeiter')
context.logger.info(f"Advoware Mitarbeiter abgerufen: {len(result) if isinstance(result, list) else 'unbekannt'}")
print(f"Advoware Mitarbeiter abgerufen: {len(result) if isinstance(result, list) else 'unbekannt'}")
return result if isinstance(result, list) else []
except Exception as e:
context.logger.error(f"Fehler beim Abrufen der Mitarbeiter: {e}")
print(f"Fehler beim Abrufen der Mitarbeiter: {e}")
return []
async def ensure_google_calendar(service, employee_kuerzel, context):
@@ -67,7 +68,7 @@ async def ensure_google_calendar(service, employee_kuerzel, context):
calendar_list = service.calendarList().list().execute()
for calendar in calendar_list.get('items', []):
if calendar['summary'] == calendar_name:
context.logger.info(f"Google Calendar '{calendar_name}' existiert bereits")
print(f"Google Calendar '{calendar_name}' existiert bereits")
return calendar['id']
# Neuen Kalender erstellen
@@ -79,12 +80,12 @@ async def ensure_google_calendar(service, employee_kuerzel, context):
created_calendar = service.calendars().insert(body=calendar_body).execute()
calendar_id = created_calendar['id']
context.logger.info(f"Google Calendar '{calendar_name}' erstellt mit ID: {calendar_id}")
print(f"Google Calendar '{calendar_name}' erstellt mit ID: {calendar_id}")
return calendar_id
except Exception as e:
context.logger.error(f"Fehler bei Google Calendar für {employee_kuerzel}: {e}")
print(f"Fehler bei Google Calendar für {employee_kuerzel}: {e}")
return None
async def get_advoware_appointments(employee_kuerzel, context):
@@ -103,10 +104,10 @@ async def get_advoware_appointments(employee_kuerzel, context):
}
result = await advoware.api_call('Termine', method='GET', params=params)
appointments = result if isinstance(result, list) else []
context.logger.info(f"Advoware Termine für {employee_kuerzel}: {len(appointments)} gefunden")
print(f"Advoware Termine für {employee_kuerzel}: {len(appointments)} gefunden")
return appointments
except Exception as e:
context.logger.error(f"Fehler beim Abrufen der Termine für {employee_kuerzel}: {e}")
print(f"Fehler beim Abrufen der Termine für {employee_kuerzel}: {e}")
return []
async def get_google_events(service, calendar_id, context):
@@ -125,10 +126,10 @@ async def get_google_events(service, calendar_id, context):
).execute()
events = events_result.get('items', [])
context.logger.info(f"Google Calendar Events: {len(events)} gefunden")
print(f"Google Calendar Events: {len(events)} gefunden")
return events
except Exception as e:
context.logger.error(f"Fehler beim Abrufen der Google Events: {e}")
print(f"Fehler beim Abrufen der Google Events: {e}")
return []
async def sync_appointment_to_google(service, calendar_id, appointment, full_content, context):
@@ -171,11 +172,11 @@ async def sync_appointment_to_google(service, calendar_id, appointment, full_con
# Event erstellen
created_event = service.events().insert(calendarId=calendar_id, body=event_body).execute()
context.logger.info(f"Termin {appointment.get('frNr')} zu Google Calendar hinzugefügt")
print(f"Termin {appointment.get('frNr')} zu Google Calendar hinzugefügt")
return created_event
except Exception as e:
context.logger.error(f"Fehler beim Sync zu Google für Termin {appointment.get('frNr')}: {e}")
print(f"Fehler beim Sync zu Google für Termin {appointment.get('frNr')}: {e}")
return None
async def sync_event_to_advoware(service, calendar_id, event, employee_kuerzel, context):
@@ -219,11 +220,11 @@ async def sync_event_to_advoware(service, calendar_id, event, employee_kuerzel,
event['extendedProperties']['private']['advoware_frnr'] = str(new_frnr)
service.events().update(calendarId=calendar_id, eventId=event['id'], body=event).execute()
context.logger.info(f"Neuer Advoware Termin erstellt: {new_frnr}, frNr in Google aktualisiert")
print(f"Neuer Advoware Termin erstellt: {new_frnr}, frNr in Google aktualisiert")
return new_frnr
except Exception as e:
context.logger.error(f"Fehler beim Sync zu Advoware für Google Event {event.get('id')}: {e}")
print(f"Fehler beim Sync zu Advoware für Google Event {event.get('id')}: {e}")
return None
async def handler(req, context):
@@ -232,10 +233,20 @@ async def handler(req, context):
body = req.get('body', {})
full_content = body.get('full_content', True) # Default: volle Termindetails
context.logger.info(f"Starte Advoware Calendar Sync, full_content: {full_content}")
print(f"Starte Advoware Calendar Sync, full_content: {full_content}")
# Google Calendar Service initialisieren
service = await get_google_service()
service = await get_google_service(context)
if not service:
print("Google Calendar Service nicht verfügbar. Sync wird übersprungen.")
return {
'status': 200,
'body': {
'status': 'skipped',
'reason': 'Google Calendar credentials not configured',
'total_synced': 0
}
}
# Alle Mitarbeiter abrufen
employees = await get_advoware_employees(context)
@@ -248,10 +259,10 @@ async def handler(req, context):
for employee in employees:
kuerzel = employee.get('kuerzel') or employee.get('anwalt')
if not kuerzel:
context.logger.warning(f"Mitarbeiter ohne Kürzel übersprungen: {employee}")
print(f"Mitarbeiter ohne Kürzel übersprungen: {employee}")
continue
context.logger.info(f"Verarbeite Mitarbeiter: {kuerzel}")
print(f"Verarbeite Mitarbeiter: {kuerzel}")
# Google Calendar sicherstellen
calendar_id = await ensure_google_calendar(service, kuerzel, context)
@@ -275,7 +286,7 @@ async def handler(req, context):
for event in google_events:
await sync_event_to_advoware(service, calendar_id, event, kuerzel, context)
context.logger.info(f"Advoware Calendar Sync abgeschlossen. {total_synced} Termine synchronisiert.")
print(f"Advoware Calendar Sync abgeschlossen. {total_synced} Termine synchronisiert.")
return {
'status': 200,
@@ -287,7 +298,7 @@ async def handler(req, context):
}
except Exception as e:
context.logger.error(f"Fehler beim Advoware Calendar Sync: {e}")
print(f"Fehler beim Advoware Calendar Sync: {e}")
return {
'status': 500,
'body': {

1
bitbylaw/types.d.ts vendored
View File

@@ -24,5 +24,6 @@ declare module 'motia' {
'Advoware Proxy POST': ApiRouteHandler<Record<string, unknown>, unknown, never>
'Advoware Proxy GET': ApiRouteHandler<Record<string, unknown>, unknown, never>
'Advoware Proxy DELETE': ApiRouteHandler<Record<string, unknown>, unknown, never>
'Advoware Calendar Sync': ApiRouteHandler<Record<string, unknown>, unknown, never>
}
}