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 ### Automatische Kalender-Erstellung
- Für jeden Advoware-Mitarbeiter wird ein Google Calendar mit dem Namen `AW-{Kuerzel}` erstellt - Für jeden Advoware-Mitarbeiter wird ein Google Calendar mit dem Namen `AW-{Kuerzel}` erstellt
- Beispiel: Mitarbeiter mit Kürzel "SB" → Calendar "AW-SB" - 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 #### 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 - Neue Termine werden in den entsprechenden Google Calendar eingetragen
- Die Advoware-Termin-ID (`frNr`) wird als Metadaten gespeichert - Die Advoware-Termin-ID (`frNr`) wird als Metadaten gespeichert
- Bestehende Termine werden aktualisiert, wenn Änderungen erkannt werden
#### Google Calendar → Advoware #### Google Calendar → Advoware
- Termine aus Google Calendar ohne `frNr` werden als neue Termine in Advoware erstellt - Termine aus Google Calendar ohne `frNr` werden als neue Termine in Advoware erstellt
- Die generierte `frNr` wird zurück in den Google Calendar geschrieben - 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 ### Token-Sicherheit
- **Vollständige Termindetails**: Titel, Beschreibung, Ort werden synchronisiert - MD5-Hash mit Salt (aus Umgebungsvariable `CALENDAR_SYNC_SALT`) für Änderungsvalidierung
- **Nur "Blocked"**: Termine werden nur als "beschäftigt" markiert (keine Details) - 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 ## API-Endpunkte
### Advoware ### Advoware API
- `GET /api/v1/advonet/Mitarbeiter` - Liste aller Mitarbeiter - `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 - `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 ### Google Calendar API
- Kalender-Management (erstellen, auflisten) - Kalender-Management (erstellen, auflisten, ACL setzen)
- Event-Management (erstellen, aktualisieren, löschen) - Event-Management (erstellen, aktualisieren, löschen)
- Service Account Authentifizierung mit Backoff bei Rate Limits
## Step-Konfiguration ## 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 - **Type:** api
- **Path:** `/advoware/calendar/sync` - **Path:** `/advoware/calendar/sync`
- **Method:** POST - **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 ## Setup
### Google API Credentials ### Google API Credentials
1. Google Cloud Console Projekt erstellen 1. Google Cloud Console Projekt erstellen
2. Google Calendar API aktivieren 2. Google Calendar API aktivieren
3. OAuth 2.0 Credentials erstellen 3. Service Account erstellen (kein OAuth)
4. `token.pickle` Datei im Projektverzeichnis bereitstellen 4. `service-account.json` Datei im Projektverzeichnis bereitstellen
### Advoware API Credentials
OAuth-ähnliche Authentifizierung mit HMAC-Signature.
### Umgebungsvariablen ### Umgebungsvariablen
```env ```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 ## Verwendung
@@ -76,28 +141,35 @@ curl -X POST "http://localhost:3000/advoware/calendar/sync" \
-d '{"full_content": true}' -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 ### 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 ## Datenmapping
### Advoware → Google Calendar ### Advoware → Google Calendar
```javascript ```javascript
{ {
"summary": appointment.text || "Advoware Termin", "summary": "Advoware blocked", // Immer "blocked" für Privacy
"description": `Advoware Termin\nNotiz: ${appointment.notiz}\nOrt: ${appointment.ort}`, "description": `Advoware Termin ID: ${appointment.frNr}`,
"location": appointment.ort, "location": "",
"start": { "start": {
"dateTime": `${appointment.datum}T${appointment.uhrzeitBis || '00:00:00'}`, "dateTime": start_datetime, // Berechnet aus datum/uhrzeitVon
"timeZone": "Europe/Berlin" "timeZone": "Europe/Berlin"
}, },
"end": { "end": {
"dateTime": `${appointment.datumBis || appointment.datum}T${appointment.uhrzeitBis || '23:59:59'}`, "dateTime": end_datetime, // Berechnet aus datumBis/uhrzeitBis
"timeZone": "Europe/Berlin" "timeZone": "Europe/Berlin"
}, },
"extendedProperties": { "extendedProperties": {
"private": { "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 ### Google Calendar → Advoware
```javascript ```javascript
{ {
"frNr": int(frnr),
"text": event.summary, "text": event.summary,
"notiz": event.description, "notiz": updated_description, // Mit sync-info
"ort": event.location, "ort": event.location,
"datum": event.start.dateTime.substring(0, 10), "datum": start.split('+')[0] if '+' in start else start,
"uhrzeitBis": event.start.dateTime.substring(11, 19), "uhrzeitBis": end.split('T')[1].split('+')[0] if 'T' in end else '09:00:00',
"datumBis": event.end.dateTime.substring(0, 10), "datumBis": end.split('+')[0] if '+' in end else end,
"sb": employee_kuerzel, "sb": employee_kuerzel,
"anwalt": employee_kuerzel "anwalt": employee_kuerzel,
"vorbereitungsDauer": "00:00:00"
} }
``` ```
## Fehlerbehandlung ## Fehlerbehandlung
- **Google API Fehler:** Automatische Token-Refresh, Fallback bei Authentifizierungsfehlern - **Google API Fehler:** Exponentieller Backoff bei 403/429, automatische Wiederholung
- **Advoware API Fehler:** Retry-Logic, detaillierte Logging - **Advoware API Fehler:** Token-Refresh bei 401, detaillierte Logging
- **Netzwerkfehler:** Timeout-Handling, Wiederholungsversuche - **JSON Parse Fehler:** Response-Text logging für Debugging
- **Dateninkonsistenzen:** Validierung vor Sync, Konfliktlösung - **Netzwerkfehler:** Timeout-Handling, konfigurierbare Timeouts
- **Dateninkonsistenzen:** Token-Validierung, Änderungsvergleich vor Update
## Monitoring ## Monitoring
### Logs ### Logs
- Erfolgreiche Synchronisationen - Erfolgreiche Synchronisationen mit Anzahl
- Fehlerhafte Termine - Fehlerhafte API-Calls mit Details
- Kalender-Erstellungen - Kalender-Erstellungen und ACL-Setups
- Performance-Metriken - Performance-Metriken (Sync-Dauer)
- Debug-Logs für Änderungsvergleiche
### Metriken ### Metriken
- Anzahl synchronisierter Termine - Anzahl synchronisierter Termine
- Verarbeitete Mitarbeiter - Verarbeitete Mitarbeiter
- Fehlerquoten - Fehlerquoten pro API
- Sync-Dauer - Gelöschte Termine
- Token-Validierungsfehler
## Sicherheit ## Sicherheit
- OAuth 2.0 für Google API Zugriff - Service Account für Google API (kein User-Consent)
- Token-Speicherung in verschlüsselten Dateien - HMAC-SHA512 für Advoware Token-Generierung
- Scoped API Permissions (nur Calendar-Zugriff) - 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 - 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 ## Erweiterungen
### Geplante Features ### Geplante Features
- Inkrementelle Syncs (nur geänderte Termine) - Inkrementelle Syncs (nur geänderte Termine seit letztem Sync)
- Konfliktlösungsstrategien (Advoware gewinnt, Google gewinnt, Manuell) - Konfliktlösungsstrategien (Advoware gewinnt, Google gewinnt, Manuell)
- Batch-Verarbeitung für Performance - Batch-Verarbeitung für bessere Performance
- Webhook-Integration für Echtzeit-Syncs - Webhook-Integration für Echtzeit-Syncs
- Mehrere Google-Accounts unterstützen - Mehrere Google-Accounts unterstützen
- Outlook/Apple Calendar Integration
### Integration mit anderen Systemen ### Code-Verbesserungen
- Outlook Calendar - Unit-Tests für API-Calls
- Apple Calendar - Mock-Server für Testing
- Mobile Apps - Config-Validation beim Startup
- Notification-Systeme - Health-Checks für beide APIs
- Metrics-Export (Prometheus)

View File

@@ -22,7 +22,7 @@ config = {
SCOPES = ['https://www.googleapis.com/auth/calendar'] SCOPES = ['https://www.googleapis.com/auth/calendar']
async def get_google_service(): async def get_google_service(context):
"""Initialisiert Google Calendar API Service""" """Initialisiert Google Calendar API Service"""
creds = None creds = None
@@ -38,7 +38,8 @@ async def get_google_service():
else: else:
# Hier würde normalerweise der OAuth Flow laufen # Hier würde normalerweise der OAuth Flow laufen
# Für Server-Umgebung brauchen wir Service Account oder gespeicherte Credentials # 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 # Token speichern
with open('token.pickle', 'wb') as token: with open('token.pickle', 'wb') as token:
@@ -52,10 +53,10 @@ async def get_advoware_employees(context):
try: try:
# Annahme: Mitarbeiter-Endpoint existiert ähnlich wie andere # Annahme: Mitarbeiter-Endpoint existiert ähnlich wie andere
result = await advoware.api_call('Mitarbeiter') 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 [] return result if isinstance(result, list) else []
except Exception as e: except Exception as e:
context.logger.error(f"Fehler beim Abrufen der Mitarbeiter: {e}") print(f"Fehler beim Abrufen der Mitarbeiter: {e}")
return [] return []
async def ensure_google_calendar(service, employee_kuerzel, context): 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() calendar_list = service.calendarList().list().execute()
for calendar in calendar_list.get('items', []): for calendar in calendar_list.get('items', []):
if calendar['summary'] == calendar_name: 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'] return calendar['id']
# Neuen Kalender erstellen # 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() created_calendar = service.calendars().insert(body=calendar_body).execute()
calendar_id = created_calendar['id'] 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 return calendar_id
except Exception as e: 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 return None
async def get_advoware_appointments(employee_kuerzel, context): 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) result = await advoware.api_call('Termine', method='GET', params=params)
appointments = result if isinstance(result, list) else [] 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 return appointments
except Exception as e: 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 [] return []
async def get_google_events(service, calendar_id, context): async def get_google_events(service, calendar_id, context):
@@ -125,10 +126,10 @@ async def get_google_events(service, calendar_id, context):
).execute() ).execute()
events = events_result.get('items', []) 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 return events
except Exception as e: 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 [] return []
async def sync_appointment_to_google(service, calendar_id, appointment, full_content, context): 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 # Event erstellen
created_event = service.events().insert(calendarId=calendar_id, body=event_body).execute() 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 return created_event
except Exception as e: 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 return None
async def sync_event_to_advoware(service, calendar_id, event, employee_kuerzel, context): 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) event['extendedProperties']['private']['advoware_frnr'] = str(new_frnr)
service.events().update(calendarId=calendar_id, eventId=event['id'], body=event).execute() 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 return new_frnr
except Exception as e: 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 return None
async def handler(req, context): async def handler(req, context):
@@ -232,10 +233,20 @@ async def handler(req, context):
body = req.get('body', {}) body = req.get('body', {})
full_content = body.get('full_content', True) # Default: volle Termindetails 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 # 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 # Alle Mitarbeiter abrufen
employees = await get_advoware_employees(context) employees = await get_advoware_employees(context)
@@ -248,10 +259,10 @@ async def handler(req, context):
for employee in employees: for employee in employees:
kuerzel = employee.get('kuerzel') or employee.get('anwalt') kuerzel = employee.get('kuerzel') or employee.get('anwalt')
if not kuerzel: if not kuerzel:
context.logger.warning(f"Mitarbeiter ohne Kürzel übersprungen: {employee}") print(f"Mitarbeiter ohne Kürzel übersprungen: {employee}")
continue continue
context.logger.info(f"Verarbeite Mitarbeiter: {kuerzel}") print(f"Verarbeite Mitarbeiter: {kuerzel}")
# Google Calendar sicherstellen # Google Calendar sicherstellen
calendar_id = await ensure_google_calendar(service, kuerzel, context) calendar_id = await ensure_google_calendar(service, kuerzel, context)
@@ -275,7 +286,7 @@ async def handler(req, context):
for event in google_events: for event in google_events:
await sync_event_to_advoware(service, calendar_id, event, kuerzel, context) 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 { return {
'status': 200, 'status': 200,
@@ -287,7 +298,7 @@ async def handler(req, context):
} }
except Exception as e: except Exception as e:
context.logger.error(f"Fehler beim Advoware Calendar Sync: {e}") print(f"Fehler beim Advoware Calendar Sync: {e}")
return { return {
'status': 500, 'status': 500,
'body': { '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 POST': ApiRouteHandler<Record<string, unknown>, unknown, never>
'Advoware Proxy GET': 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 Proxy DELETE': ApiRouteHandler<Record<string, unknown>, unknown, never>
'Advoware Calendar Sync': ApiRouteHandler<Record<string, unknown>, unknown, never>
} }
} }