# Technische Dokumentation ## Notion → Apple Kalender / Exchange Sync **Version:** 1.1 **Stand:** Mai 2026 **Lösungsansatz:** Python + AppleScript → Apple Kalender → Exchange Online ### Änderungshistorie | Version | Änderung | |---------|----------| | 1.0 | Erstversion | | 1.1 | (1) Update-Modus für bestehende Termine ergänzt; (2) Termine pro Tag laufen sequenziell (back-to-back), nicht parallel; (3) Notion-Eigenschaft umbenannt: „Completion Date" → „Last Edit" | --- ## Inhaltsverzeichnis 1. [Projektübersicht](#1-projektübersicht) 2. [Systemumgebung](#2-systemumgebung) 3. [Beabsichtigtes Verhalten (Functional Specification)](#3-beabsichtigtes-verhalten) 4. [Architektur & Datenfluss](#4-architektur--datenfluss) 5. [Notion-Datenmodell](#5-notion-datenmodell) 6. [Datenverarbeitung & Geschäftslogik](#6-datenverarbeitung--geschäftslogik) 7. [Komponente 1 – Python-Skript](#7-komponente-1--python-skript) 8. [Komponente 2 – AppleScript-Integration](#8-komponente-2--applescript-integration) 9. [Konfiguration durch den Nutzer](#9-konfiguration-durch-den-nutzer) 10. [Projektstruktur](#10-projektstruktur) 11. [Fehlerbehandlung & Logging](#11-fehlerbehandlung--logging) 12. [Testszenarien](#12-testszenarien) 13. [Ausführung & Workflow](#13-ausführung--workflow) --- ## 1. Projektübersicht ### Ziel Dieses Tool synchronisiert abgeschlossene Aufgaben aus einer Notion-Datenbank in den Microsoft Outlook-Kalender (via Exchange Online). Da auf dem System ausschließlich macOS eingesetzt wird und Apple Kalender bereits mit dem Exchange-Konto verbunden ist, erfolgt die Kalender-Integration über AppleScript. Apple Kalender übernimmt dann automatisch die Synchronisation zu Exchange/Outlook. ### Kernprobleme In Notion existieren pro Tag mehrere Einträge für verschiedene Projekte. Im Kalender soll pro Tag und Projekt genau **ein** gebündelter Termin entstehen. Mehrere Termine am selben Tag sollen **nahtlos hintereinander** liegen (back-to-back), nicht alle zur selben Zeit starten. Ändert sich nachträglich etwas in Notion (z. B. eine T-Shirt-Size wird korrigiert), soll der bestehende Kalendertermin **aktualisiert** werden. ### Abgrenzung - Das Tool läuft **nicht automatisch** im Hintergrund, sondern wird manuell am Ende eines Kalendermonats (oder bei Bedarf zwischendurch) ausgeführt. - Bestehende Kalendereinträge können **aktualisiert**, aber **nicht gelöscht** werden. - Das Tool ist **idempotent**: Bei unveränderter Notion-Datenlage passiert beim zweiten Lauf nichts. --- ## 2. Systemumgebung ### Hardware & Betriebssystem | Eigenschaft | Wert | |---------------------|----------------------------| | Betriebssystem | macOS (aktuell) | | Ausführungsumgebung | Terminal / Shell | | Sprache | Python 3.x | ### Externe Dienste & Zugänge | Dienst | Zugang / Schnittstelle | Status | |--------------------|--------------------------------|---------------------------| | Notion | Notion REST API v1 (notion.so) | Integration vorhanden | | Microsoft Exchange | Exchange Online (Arbeitgeber) | Über Apple Kalender verbunden | | Apple Kalender | AppleScript / `osascript` | Lokale App, macOS-nativ | ### Voraussetzungen auf dem System - **Python 3.8+** installiert (z. B. via Homebrew oder System-Python) - **pip**-Pakete (werden in Abschnitt 9 beschrieben) - **Apple Kalender App** ist geöffnet und mit Exchange-Konto verbunden - **Notion API Integration** mit Zugriff auf die Datenbank „Projekte" eingerichtet --- ## 3. Beabsichtigtes Verhalten ### 3.1 Trigger Der Nutzer führt das Skript manuell aus. Per Parameter wird der Zielmonat angegeben (z. B. `--monat 2026-04` für April 2026). Das Skript kann auch mehrfach für denselben Monat ausgeführt werden – neue Einträge werden erstellt, geänderte Einträge aktualisiert. ### 3.2 Datenquelle Das Skript liest alle Seiten aus der Notion-Datenbank **„Projekte"**, deren Eigenschaft `Last Edit` in den angegebenen Zielmonat fällt. ### 3.3 Aggregationslogik Mehrere Notion-Einträge, die **am gleichen Tag** zum **gleichen Projekt** gehören, werden zu **einem Kalendertermin** zusammengeführt. Würde der zusammengesetzte Titel die Outlook-Grenze von **248 Zeichen** überschreiten, werden automatisch **mehrere aufeinanderfolgende Termine** für dasselbe Projekt erzeugt – jeder mit einer Teilmenge der Aufgaben, die ins Limit passt. ### 3.4 Sequenzielle Terminreihenfolge An einem Tag mit mehreren Projekten liegen die Termine **nahtlos hintereinander**. Der erste Termin beginnt immer um **09:00 Uhr**. Jeder weitere Termin desselben Tages startet genau dort, wo der vorherige endet. **Sortierkriterium:** Die Reihenfolge der Termine pro Tag richtet sich nach dem **Projektnamen in alphabetischer Reihenfolge** (A → Z), sekundär nach der **Teilnummer** (bei aufgeteilten Projekten). Dadurch ist die Reihenfolge bei jedem Lauf identisch und reproduzierbar – auch bei nachträglichen Updates. **Beispiel:** | Notion-Eintrag | Last Edit | Project | T-Shirt Size | |----------------|------------|-----------|--------------| | Aufgabe A | 2026-04-03 | EfA-RI NI | S (30 min) | | Aufgabe B | 2026-04-03 | EfA-RI NI | M (60 min) | | Aufgabe C | 2026-04-03 | DiPlanung | XS (15 min) | **Aggregation (Summen pro Projekt):** | Projekt | Gesamtdauer | |-----------|-------------| | DiPlanung | 15 min | | EfA-RI NI | 90 min | **Sortierung:** DiPlanung (D) kommt vor EfA-RI NI (E). **Ergebnis im Kalender:** | Kalendertermin | Datum | Projekt | Startzeit | Endzeit | Berechnung | |----------------|------------|-----------|-----------|---------|-----------------------------------| | Termin 1 | 03.04.2026 | DiPlanung | 09:00 | 09:15 | 09:00 + 15 min | | Termin 2 | 03.04.2026 | EfA-RI NI | 09:15 | 10:45 | 09:15 (Ende Termin 1) + 90 min | ### 3.5 Update-Verhalten Das Skript unterscheidet bei jedem ermittelten Kalendertermin drei Zustände: | Zustand | Bedingung | Aktion | |------------------|----------------------------------------------------------------------------|-----------------------------------------------| | **Neu** | Kein Eintrag in der Protokolldatei für diese Datum+Projekt-Kombination | Termin wird neu erstellt | | **Unverändert** | Protokolleintrag vorhanden, Inhalt identisch (Dauer, Notizen, Zeiten) | Kein API-Call, wird übersprungen | | **Geändert** | Protokolleintrag vorhanden, aber Inhalt hat sich verändert | Termin wird via UID in Apple Kalender gesucht und aktualisiert | > **Warum UID?** Apple Kalender vergibt beim Erstellen eines Termins eine eindeutige `uid`. Diese wird in der Protokolldatei gespeichert. Bei einem Update-Lauf sucht das AppleScript den Termin anhand dieser UID und überschreibt dessen Eigenschaften – kein Löschen und Neuerstellen nötig. > **Wichtiger Seiteneffekt der sequenziellen Reihenfolge:** Ändert sich die Dauer eines Projekts an einem Tag, verschieben sich alle nachfolgenden Termine dieses Tages. Das Skript **berechnet alle Startzeiten eines Tages neu** und aktualisiert alle betroffenen Termine – auch solche, deren eigene Notion-Daten unverändert sind. ### 3.6 Kalendertermin-Eigenschaften | Feld | Inhalt | |---------------|---------------------------------------------------------------------------| | **Titel** | Name des Projekts (z. B. `EfA-RI NI`) | | **Datum** | `Last Edit`-Datum aus Notion | | **Startzeit** | Berechneter Beginn (09:00 für ersten Termin, Folgezeiten sequenziell) | | **Endzeit** | Startzeit + Summe aller T-Shirt-Size-Minuten | | **Notizen** | Aufzählung aller enthaltenen Notion-Aufgabentitel + Größen | | **Kalender** | Der Exchange-Kalender in Apple Kalender (konfigurierbar) | ### 3.7 Nicht abgedeckte Fälle - T-Shirt-Size fehlt bei einem Eintrag → Eintrag wird übersprungen und geloggt - `Last Edit` fehlt → Eintrag wird übersprungen und geloggt - Projekt-Relation fehlt → Eintrag wird übersprungen und geloggt - Termin wurde manuell in Apple Kalender gelöscht, UID existiert nicht mehr → Termin wird neu erstellt, neue UID gespeichert --- ## 4. Architektur & Datenfluss ``` ┌─────────────────────────────────────────────────────────┐ │ PYTHON-SKRIPT │ │ │ │ 1. Konfiguration laden (config.json) │ │ 2. Protokolldatei laden (sync_state.json) │ │ 3. Notion API abfragen (Datenbank „Projekte") │ │ 4. Rohdaten filtern (nach Monat) │ │ 5. Daten aggregieren (pro Tag + Projekt) │ │ 6. Sequenzielle Startzeiten pro Tag berechnen │ │ 7. Für jeden Termin: Zustand bestimmen │ │ (neu / unverändert / geändert) │ │ 8a. Neu → AppleScript CREATE ausführen │ │ 8b. Geändert → AppleScript UPDATE (per UID) ausführen │ │ 8c. Unverändert → überspringen │ │ 9. Protokolldatei aktualisieren (UIDs speichern) │ └────────────────────┬────────────────────────────────────┘ │ osascript (subprocess) ▼ ┌─────────────────────────────────────────────────────────┐ │ APPLE KALENDER APP │ │ │ │ - CREATE: Neuen Termin anlegen, UID zurückgeben │ │ - UPDATE: Termin per UID suchen, Felder überschreiben │ │ - Synchronisiert automatisch mit Exchange Online │ └─────────────────────────────────────────────────────────┘ │ Exchange ActiveSync / EWS ▼ ┌─────────────────────────────────────────────────────────┐ │ MICROSOFT EXCHANGE ONLINE │ │ → sichtbar in Outlook / OWA │ └─────────────────────────────────────────────────────────┘ ``` --- ## 5. Notion-Datenmodell ### Datenbank: „Projekte" | Eigenschaft | Notion-Typ | Beschreibung | |----------------|------------|------------------------------------------------------------| | `Name` | Title | Bezeichnung der Aufgabe (frei) | | `T-Shirt Size` | Select | Dauer der Aufgabe (XS / S / M / L / XL) | | `Last Edit` | Date | Datum, an dem die Aufgabe abgeschlossen wurde | | `Project` | Relation | Verknüpfung zu einer Projekt-Datenbank; liefert ID + Name | ### T-Shirt-Size-Mapping | Select-Wert | Minuten | |-------------|---------| | XS | 15 | | S | 30 | | M | 60 | | L | 120 | | XL | 240 | Dieses Mapping wird in der Konfigurationsdatei (`config.json`) hinterlegt und kann dort einfach angepasst werden. ### Notion API – Relevante Endpoints - **Datenbank abfragen:** `POST https://api.notion.com/v1/databases/{database_id}/query` - **Filter:** `Last Edit` between Monatsanfang und Monatsende - **API-Version:** `2022-06-28` ### Beispiel-API-Filter (JSON-Body) ```json { "filter": { "and": [ { "property": "Last Edit", "date": { "on_or_after": "2026-04-01" } }, { "property": "Last Edit", "date": { "on_or_before": "2026-04-30" } } ] } } ``` ### Pagination Die Notion API gibt maximal 100 Ergebnisse pro Anfrage zurück. Das Skript muss über den `next_cursor`-Mechanismus alle Seiten vollständig einlesen (Pagination-Loop). ### Relation-Feld auslesen Das Feld `Project` liefert im API-Response eine Liste von IDs. Um den Projektnamen zu erhalten, muss das Skript für jede eindeutige Projekt-ID einmalig `GET https://api.notion.com/v1/pages/{page_id}` aufrufen und den `title`-Wert extrahieren. Die Ergebnisse werden innerhalb eines Laufs gecacht, um API-Calls zu minimieren. --- ## 6. Datenverarbeitung & Geschäftslogik ### 6.1 Datenabruf ``` notion_pages = query_notion_database( database_id = config["notion_database_id"], month_start = "YYYY-MM-01", month_end = "YYYY-MM-{letzter_tag}" ) ``` ### 6.2 Normalisierung je Notion-Seite Für jede geladene Notion-Seite werden folgende Felder extrahiert: ```python { "title": str, # Name der Notion-Seite "t_shirt_size": str, # z. B. "M" "minutes": int, # aus Mapping: 60 "last_edit": date, # Python date-Objekt "project_id": str, # Notion-Page-ID des Projekts "project_name": str # aufgelöst via separatem API-Call + Cache } ``` ### 6.3 Aggregation **Schritt 1 – Gruppierung** nach Schlüssel **(last_edit, project_name)**: Für jede Gruppe wird ermittelt: - `total_minutes` = Summe aller `minutes`-Werte - `notes` = Aufzählung aller Aufgabentitel + T-Shirt-Größe, z. B.: ``` - Konzept erstellt (S) - Review-Meeting vorbereitet (M) ``` **Schritt 2 – Tagesweise Sortierung und Startzeitberechnung:** Alle Gruppen eines Tages werden **alphabetisch nach `project_name`** sortiert. Anschließend werden die Startzeiten sequenziell berechnet: ```python day_start_hour = config["day_start_hour"] # Standard: 9 cursor = datetime(year, month, day, day_start_hour, 0) for event in sorted_events_for_day: event["start_time"] = cursor event["end_time"] = cursor + timedelta(minutes=event["total_minutes"]) cursor = event["end_time"] # nächster Termin beginnt hier ``` **Ergebnis je Kalender-Event:** ```python { "project_name": str, "date": date, "start_time": time, # berechnete Startzeit "end_time": time, # berechnete Endzeit "total_minutes": int, "notes": str } ``` ### 6.4 Zustandsbestimmung (neu / unverändert / geändert) Die Protokolldatei `sync_state.json` enthält für jede bereits verarbeitete Datum+Projekt-Kombination einen Eintrag: ```json { "2026-04-03|DiPlanung": { "uid": "ABC123-...", "start_time": "09:00", "end_time": "09:15", "total_minutes": 15, "notes": "- Aufgabe C (XS)" }, "2026-04-03|EfA-RI NI": { "uid": "DEF456-...", "start_time": "09:15", "end_time": "10:45", "total_minutes": 90, "notes": "- Aufgabe A (S)\n- Aufgabe B (M)" } } ``` **Vergleichslogik pro ermitteltem Event:** ``` key = "YYYY-MM-DD|Projektname" if key not in protocol: → Zustand: NEU elif protocol[key].total_minutes != event.total_minutes or protocol[key].notes != event.notes or protocol[key].start_time != event.start_time: → Zustand: GEÄNDERT (uid aus Protokoll übernehmen) else: → Zustand: UNVERÄNDERT ``` > **Hinweis zu Startzeitenänderungen:** Da Startzeiten von anderen Projekten desselben Tages abhängen, kann ein Event als GEÄNDERT gelten, auch wenn seine eigenen Notion-Daten unverändert sind (weil ein Vorgänger-Termin eine andere Dauer bekommen hat). Dies ist korrekt – der Termin muss dann verschoben werden. ### 6.5 UID-Rückgabe bei CREATE Das AppleScript gibt nach dem Erstellen eines neuen Termins dessen `uid` als String zurück. Python liest diesen Wert aus dem `stdout` des `osascript`-Prozesses und speichert ihn in der Protokolldatei. --- ## 7. Komponente 1 – Python-Skript ### Dateiname: `notion_to_calendar.py` ### Abhängigkeiten (pip) ``` requests>=2.31.0 python-dateutil>=2.8.2 ``` Installation: `pip install requests python-dateutil` ### Kommandozeilenparameter | Parameter | Pflicht | Beschreibung | Beispiel | |--------------|---------|-----------------------------------------------------|---------------------| | `--monat` | Ja | Zielmonat im Format YYYY-MM | `--monat 2026-04` | | `--dry-run` | Nein | Zeigt Termine an, ohne sie anzulegen oder zu ändern | `--dry-run` | | `--kalender` | Nein | Name des Zielkalenders (überschreibt config.json) | `--kalender "Work"` | ### Programmablauf (Pseudocode) ``` 1. Konfiguration laden aus config.json 2. Kommandozeilenparameter parsen 3. Monatsanfang und Monatsende berechnen 4. Protokolldatei laden (sync_state.json), falls vorhanden 5. Notion-Datenbank abfragen (mit Pagination) 6. Für jede Notion-Seite: a. Pflichtfelder prüfen (Last Edit, T-Shirt Size, Project) b. Falls fehlend: loggen, überspringen c. Daten normalisieren d. Projekt-ID → Projektname auflösen (mit Cache) 7. Daten aggregieren (gruppiert nach Last Edit + Projektname) 8. Für jeden Tag: Projekte alphabetisch sortieren, Startzeiten sequenziell berechnen 9. Für jedes Event: Zustand bestimmen (neu / unverändert / geändert) 10. Im --dry-run-Modus: alle Events tabellarisch mit Zustand ausgeben, dann Abbruch 11. Für jedes Event (nicht dry-run): a. UNVERÄNDERT → überspringen, loggen b. NEU → AppleScript CREATE ausführen → UID aus stdout lesen → Protokolldatei aktualisieren c. GEÄNDERT → AppleScript UPDATE (mit gespeicherter UID) ausführen → Bei "not found"-Fehler: Fallback auf CREATE, neue UID speichern → Protokolldatei aktualisieren 12. Zusammenfassung ausgeben (X erstellt, Y aktualisiert, Z unverändert, W Fehler) ``` ### Wichtige Python-Funktionen (zu implementieren) ```python def load_config(path: str) -> dict # Lädt config.json, validiert Pflichtfelder def get_month_bounds(month_str: str) -> tuple[date, date] # "2026-04" → (date(2026,4,1), date(2026,4,30)) def query_notion_database(database_id: str, token: str, start: date, end: date) -> list[dict] # Notion API mit Pagination; gibt rohe page-Objekte zurück def resolve_project_name(page_id: str, token: str, cache: dict) -> str | None # Löst Projekt-ID → Name auf; nutzt Cache def normalize_page(page: dict, token: str, cache: dict, size_map: dict) -> dict | None # Extrahiert und normalisiert Felder; None bei fehlendem Pflichtfeld def aggregate_events(pages: list[dict]) -> dict[date, list[dict]] # Gruppiert nach (date, project) und summiert Minuten # Gibt dict: date → Liste von Event-Dicts (noch ohne Startzeiten) def assign_sequential_times(events_by_day: dict, day_start_hour: int) -> list[dict] # Sortiert pro Tag alphabetisch, berechnet Startzeiten sequenziell # Gibt flache Liste aller Events mit start_time und end_time zurück def determine_state(event: dict, protocol: dict) -> str # Gibt "new", "unchanged" oder "changed" zurück def build_create_script(event: dict, calendar_name: str) -> str # Generiert AppleScript für neuen Termin; Script gibt UID auf stdout zurück def build_update_script(event: dict, uid: str, calendar_name: str) -> str # Generiert AppleScript für Update eines bestehenden Termins per UID def run_applescript(script: str) -> tuple[bool, str] # Führt AppleScript via subprocess aus # Gibt (Erfolg: bool, stdout: str) zurück def load_protocol(path: str) -> dict # Lädt sync_state.json; gibt leeres dict zurück, wenn Datei fehlt def save_protocol(path: str, protocol: dict) -> None # Schreibt aktualisiertes dict als JSON zurück def escape_applescript_string(s: str) -> str # Escaped Sonderzeichen für AppleScript-Strings ``` --- ## 8. Komponente 2 – AppleScript-Integration ### Prinzip Das Python-Skript generiert AppleScript-Strings und führt diese via `subprocess.run(["osascript", "-e", script])` aus. Es gibt zwei Script-Templates: eines für **CREATE** (gibt UID zurück), eines für **UPDATE** (sucht Event per UID und überschreibt Felder). ### AppleScript-Template: CREATE Das Script erstellt den Termin und gibt dessen `uid` auf stdout aus. Python liest diesen Wert aus. ```applescript tell application "Calendar" tell calendar "{CALENDAR_NAME}" set startDate to current date set year of startDate to {YEAR} set month of startDate to {MONTH} set day of startDate to {DAY} set hours of startDate to {START_HOUR} set minutes of startDate to {START_MINUTE} set seconds of startDate to 0 set endDate to current date set year of endDate to {YEAR} set month of endDate to {MONTH} set day of endDate to {DAY} set hours of endDate to {END_HOUR} set minutes of endDate to {END_MINUTE} set seconds of endDate to 0 set newEvent to make new event with properties {¬ summary:"{PROJECT_NAME}", ¬ start date:startDate, ¬ end date:endDate, ¬ description:"{NOTES}"} return uid of newEvent end tell end tell ``` Python liest die UID aus stdout: ```python result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True) uid = result.stdout.strip() ``` ### AppleScript-Template: UPDATE Das Script sucht den Termin anhand der gespeicherten UID und überschreibt alle relevanten Felder. ```applescript tell application "Calendar" tell calendar "{CALENDAR_NAME}" set matchingEvents to (every event whose uid is "{UID}") if (count of matchingEvents) = 0 then return "ERROR: not found" end if set theEvent to item 1 of matchingEvents set startDate to current date set year of startDate to {YEAR} set month of startDate to {MONTH} set day of startDate to {DAY} set hours of startDate to {START_HOUR} set minutes of startDate to {START_MINUTE} set seconds of startDate to 0 set endDate to current date set year of endDate to {YEAR} set month of endDate to {MONTH} set day of endDate to {DAY} set hours of endDate to {END_HOUR} set minutes of endDate to {END_MINUTE} set seconds of endDate to 0 set start date of theEvent to startDate set end date of theEvent to endDate set description of theEvent to "{NOTES}" return "OK" end tell end tell ``` ### Rückgabewert-Auswertung (UPDATE) ```python success, stdout = run_applescript(script) if not success or stdout.strip().startswith("ERROR"): # Termin existiert nicht mehr → Fallback auf CREATE log_warning(f"Update fehlgeschlagen ({stdout.strip()}), lege Termin neu an.") # → build_create_script aufrufen, neue UID speichern ``` ### Sonderzeichen escapen ```python def escape_applescript_string(s: str) -> str: s = s.replace("\\", "\\\\") # Backslashes zuerst s = s.replace('"', '\\"') # Anführungszeichen s = s.replace("\n", "\\n") # Zeilenumbrüche in description return s ``` ### Kalender-Name-Validierung Existiert der konfigurierte Kalender nicht, gibt AppleScript einen Fehler zurück. Das Skript fängt diesen ab und bricht mit einer klaren Meldung ab: „Kalender '{name}' nicht gefunden – bitte calendar_name in config.json prüfen." --- ## 9. Konfiguration durch den Nutzer Die gesamte nutzerspezifische Konfiguration liegt in `config.json`. Diese Datei **darf nicht in ein öffentliches Repository eingecheckt werden**. ### Datei: `config.json` ```json { "notion_token": "secret_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "notion_database_id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "calendar_name": "NAME_DES_EXCHANGE_KALENDERS", "day_start_hour": 9, "t_shirt_size_map": { "XS": 15, "S": 30, "M": 60, "L": 120, "XL": 240 }, "protocol_file": "sync_state.json", "log_file": "notion_sync.log" } ``` **Feld `day_start_hour`:** Startzeit des ersten Termins eines Tages in Stunden (Standard: `9` für 09:00 Uhr). Kann hier ohne Code-Änderung angepasst werden. ### Schritt-für-Schritt-Anleitung zur Konfiguration #### Schritt 1 – Notion API Token ermitteln 1. Öffne [https://www.notion.so/my-integrations](https://www.notion.so/my-integrations) 2. Wähle deine bestehende Integration aus 3. Kopiere den **„Internal Integration Token"** (beginnt mit `secret_...`) 4. Trage den Wert für `"notion_token"` in `config.json` ein #### Schritt 2 – Notion Datenbank-ID ermitteln 1. Öffne die Notion-Datenbank „Projekte" im Browser 2. Die URL hat das Format: `https://www.notion.so/{workspace}/{DATABASE_ID}?v=...` 3. Kopiere die **32-stellige ID** vor dem `?v=` 4. Falls keine Bindestriche vorhanden: manuell einfügen nach Schema `8-4-4-4-12` Beispiel: `12345678901234567890123456789012` → `12345678-9012-3456-7890-123456789012` 5. Trage die ID als `"notion_database_id"` ein > **Wichtig:** Die Integration muss Datenbankzugriff haben. > In Notion: Datenbank öffnen → `...` Menü → **„Connections"** → Integration auswählen. #### Schritt 3 – Kalender-Name ermitteln 1. Öffne die **Apple Kalender App** 2. In der linken Seitenleiste alle verfügbaren Kalender prüfen 3. Den Kalender finden, der mit dem Exchange-Konto verbunden ist (erkennbar am Exchange-Icon oder der Arbeits-E-Mail-Adresse) 4. Den **exakten Namen** notieren (Groß-/Kleinschreibung und Leerzeichen beachten) 5. Als `"calendar_name"` eintragen #### Schritt 4 – Python-Abhängigkeiten installieren ```bash pip install requests python-dateutil # oder bei Homebrew-Python: pip3 install requests python-dateutil ``` --- ## 10. Projektstruktur ``` notion-to-calendar/ │ ├── notion_to_calendar.py # Haupt-Skript ├── config.json # Nutzerkonfiguration (nicht einchecken!) ├── sync_state.json # Protokolldatei mit UIDs (automatisch erstellt) ├── notion_sync.log # Log-Datei (automatisch erstellt) ├── .gitignore └── README.md ``` ### `.gitignore` (Mindestinhalt) ``` config.json sync_state.json notion_sync.log __pycache__/ *.pyc ``` --- ## 11. Fehlerbehandlung & Logging ### Fehlerklassen | Fehlertyp | Verhalten | |------------------------------------|------------------------------------------------------------------------------| | Notion API nicht erreichbar | Abbruch mit Fehlermeldung, Exit Code 1 | | Ungültiger Notion-Token | Abbruch mit Hinweis, Token in config.json zu prüfen | | Datenbank-ID nicht gefunden | Abbruch mit Hinweis auf falsche ID | | Pflichtfeld fehlt (Notion-Seite) | Seite überspringen, in Log schreiben, weitermachen | | Unbekannter T-Shirt-Size-Wert | Seite überspringen, in Log schreiben | | Apple Kalender nicht geöffnet | osascript-Fehler abfangen, Meldung ausgeben, Termin überspringen | | Kalender-Name nicht gefunden | Abbruch mit Hinweis auf falschen Kalender-Namen | | UID bei UPDATE nicht gefunden | Fallback auf CREATE, neue UID speichern, Warnung loggen | | AppleScript-Fehler (allgemein) | Termin überspringen, Fehler in Log schreiben | | Protokolldatei nicht schreibbar | Warnung ausgeben, Skript läuft weiter | ### Log-Format ``` [2026-04-30 17:42:01] INFO Starte Sync für Monat: 2026-04 [2026-04-30 17:42:02] INFO 21 Notion-Seiten geladen [2026-04-30 17:42:02] WARN Seite "Aufgabe X": T-Shirt Size fehlt – übersprungen [2026-04-30 17:42:03] INFO NEU: DiPlanung | 2026-04-03 | 09:00–09:15 [2026-04-30 17:42:03] INFO NEU: EfA-RI NI | 2026-04-03 | 09:15–10:45 [2026-04-30 17:42:05] INFO GEÄNDERT: EfA-RI NI | 2026-04-07 | 09:00–11:00 (vorher 09:00–10:30) [2026-04-30 17:42:06] INFO UNVERÄNDERT: DiPlanung | 2026-04-07 – übersprungen [2026-04-30 17:42:10] INFO Sync abgeschlossen: 10 erstellt, 3 aktualisiert, 8 unverändert, 1 Fehler ``` --- ## 12. Testszenarien ### Szenario 1 – Normalbetrieb, erster Lauf - **Input:** 2 Projekte, 3 Tage, mehrere Einträge pro Tag - **Erwartung:** Korrekte Anzahl Termine, sequenzielle Uhrzeiten, alphabetische Reihenfolge, UIDs in sync_state.json gespeichert ### Szenario 2 – Zweiter Lauf ohne Änderungen - **Input:** Identische Notion-Daten, sync_state.json bereits vorhanden - **Erwartung:** 0 Termine erstellt oder geändert; alle als „unverändert" geloggt ### Szenario 3 – Notion-Daten wurden nachträglich geändert - **Input:** T-Shirt-Size eines Eintrags für EfA-RI NI von S auf L geändert - **Erwartung:** EfA-RI NI-Termin wird aktualisiert; falls DiPlanung am gleichen Tag danach kommt, wird dessen Startzeit ebenfalls neu gesetzt (Update) ### Szenario 4 – Termin manuell in Apple Kalender gelöscht - **Input:** UID in sync_state.json vorhanden, aber Termin im Kalender nicht mehr existent - **Erwartung:** Fallback auf CREATE, Termin neu angelegt, neue UID gespeichert, Warnung im Log ### Szenario 5 – `--dry-run` - **Input:** `--monat 2026-04 --dry-run` - **Erwartung:** Tabellarische Ausgabe aller Events mit Zustand (NEU / GEÄNDERT / UNVERÄNDERT), kein AppleScript ausgeführt, sync_state.json unverändert. Oberhalb der Tabelle erscheint eine Legende. Farbkodierung: Spalte „Dauer" grün bei ≥ 480 min (8 h), rot bei > 600 min (10 h); Spalte „Datum" orange für alle Tage einer ISO-Woche, deren Gesamtminuten > 2880 (48 h). ### Szenario 6 – Fehlende Pflichtfelder - **Input:** Notion-Seite ohne `Last Edit` - **Erwartung:** Seite wird übersprungen, Warnung im Log, keine Exception ### Szenario 7 – Ungültiger Kalender-Name - **Input:** Falscher `calendar_name` in config.json - **Erwartung:** Klare Fehlermeldung beim ersten Script-Aufruf, sauberer Abbruch ### Szenario 8 – Einzelner Eintrag pro Tag/Projekt - **Input:** Genau ein Notion-Eintrag (XS = 15 min), kein zweites Projekt an diesem Tag - **Erwartung:** Termin 09:00–09:15 ### Szenario 9 – Drei Projekte an einem Tag - **Input:** Projekte „Alpha" (M=60 min), „Beta" (S=30 min), „Gamma" (XS=15 min) - **Erwartung:** - Alpha: 09:00–10:00 - Beta: 10:00–10:30 - Gamma: 10:30–10:45 ### Szenario 10 – Große Gesamtdauer - **Input:** XL + L + L an einem Tag für ein Projekt (480 min = 8h) - **Erwartung:** Termin 09:00–17:00; kein Crash auch wenn nachfolgende Termine über 17:00 hinausgehen --- ## 13. Ausführung & Workflow ### Erster Start (Einrichtung) ```bash # 1. Projektverzeichnis anlegen mkdir notion-to-calendar && cd notion-to-calendar # 2. notion_to_calendar.py ablegen # 3. config.json anlegen und befüllen (siehe Abschnitt 9) # 4. Abhängigkeiten installieren pip install requests python-dateutil # 5. Probelauf ohne echte Kalendereinträge python notion_to_calendar.py --monat 2026-04 --dry-run ``` ### Reguläre Monatsausführung ```bash # Apple Kalender sicherstellen (muss geöffnet und mit Exchange verbunden sein) open -a Calendar # Sync ausführen python notion_to_calendar.py --monat 2026-04 ``` ### Nachträgliche Korrekturen in Notion Falls nach dem Monatsabschluss Daten in Notion korrigiert wurden: ```bash # Skript erneut ausführen – geänderte Termine werden automatisch aktualisiert python notion_to_calendar.py --monat 2026-04 ``` ### Nach der Ausführung 1. Apple Kalender auf erstellte/aktualisierte Termine prüfen 2. Kurz warten (~1–5 Minuten) bis Exchange-Sync abgeschlossen 3. Termine in Outlook / OWA verifizieren 4. Log-Datei bei Bedarf prüfen: `cat notion_sync.log` --- *Ende der Dokumentation – Version 1.1*