# Design: Windows Service + Tray-GUI für journal-bot **Datum:** 2026-06-16 **Status:** Approved (brainstorming) → bereit für Implementierungsplan ## Ziel Zwei dünne .NET-Hüllen um die bestehende Python-CLI: 1. **Windows Service** holt Telegram-Nachrichten alle 15 Minuten selbstständig ab (`ingest`). 2. **Tray-GUI (WPF)** zeigt die Historie, Queue-Status und Logs und triggert `ingest`/`process`/`both` manuell. Die Python-Codebase bleibt die fachliche Schnittstelle. Service und GUI rufen sie nur über die CLI auf und lesen das `runtime/`-Dateisystem. Keine direkte IPC zwischen Service und GUI — das Dateisystem ist die Schnittstelle. ## Architektur ``` ┌─────────────────────┐ ┌──────────────────────────┐ │ JournalBot.Service │ │ JournalBot.Tray (WPF) │ │ (.NET Windows Svc, │ │ User-Prozess, @logon │ │ LocalSystem) │ │ │ │ Timer alle 15 min │ │ Tray-Icon + Fenster │ │ → python -m │ │ Tabs: Historie | Log │ │ journal_bot │ │ Status: pending/failed │ │ ingest │ │ Buttons: Ingest/Process │ │ │ │ /Both │ └──────────┬──────────┘ └───────────┬──────────────┘ │ schreibt │ liest done/, logs/ │ pending/ │ ruft python-CLI ▼ ▼ ┌──────────────────────────────────────────┐ │ runtime/ (queue/{pending,done,failed}, │ │ logs/) ← gemeinsame Schnittst. │ └──────────────────────────────────────────┘ │ python -m journal_bot (ingest / process / both) ``` **Hybrid-Aufteilung (Kernentscheidung):** - **Service** macht *nur* `ingest`. Telegram braucht keinen User-Login → sammelt zuverlässig, läuft als `LocalSystem` auch ohne eingeloggten User. - **Tray-GUI** macht `process`. Das braucht LM Studio, das als interaktive User-App nur bei eingeloggtem User läuft → daher User-Prozess, manueller Trigger. Damit umgehen wir das Session-0-Isolations-Problem (ein echter Service kann kein Tray-Icon zeichnen, und `LocalSystem` erreicht LM Studio nicht). Zwei separate .NET-Projekte in einer Solution unter `tools/`, self-contained EXEs (kein .NET-Runtime-Install nötig). ## Komponente 1: JournalBot.Service `.NET Worker Service`, echter Windows Service via `Microsoft.Extensions.Hosting.WindowsServices`. - **`Worker : BackgroundService`** — `PeriodicTimer(TimeSpan.FromMinutes(IntervalMinutes))`-Loop. Pro Tick: startet `python.exe -m journal_bot ingest` als Subprozess, wartet aufs Ende, loggt Exit-Code + stdout/stderr. - **Konfiguration** via `appsettings.json` neben der EXE: `PythonExe` (Pfad zu `.venv\Scripts\python.exe`), `WorkingDir` (Projekt-Root), `IntervalMinutes` (Default 15). - **Kontext:** `LocalSystem`. `ingest` braucht nur Telegram + Dateisystem, kein LM Studio, kein User-Profil. `WorkingDir` wird explizit gesetzt. - **Logging:** `runtime/logs/service.log` (eigene Datei) **und** Windows Event Log. - **Fehlertoleranz:** Ein fehlgeschlagener `ingest`-Aufruf wird geloggt, Loop läuft weiter. Kein Service-Crash. - **Installation:** `scripts/install-service.ps1` (`New-Service`/`sc.exe`), Service-Name `JournalBotService`. **Zu verifizieren beim Bauen:** Lädt der Bot die `.env` relativ zum CWD? Falls ja, genügt `WorkingDir` = Projekt-Root. Falls absolut, entsprechend anpassen. ## Komponente 2: JournalBot.Tray (WPF) WPF-App, `OutputType=WinExe` (kein Konsolenfenster), Autostart @logon. **Tray-Icon** (`Hardcodet.NotifyIcon.Wpf`): - Linksklick → Fenster zeigen/verstecken. Fenster schließen = Minimieren in Tray (kein Beenden). - Kontextmenü: *Ingest* · *Process* · *Both* · *Fenster öffnen* · *Beenden*. - Icon-Status: roter Indikator bei `failed > 0`. **Hauptfenster:** - **Status-Banner oben:** „X pending · Y failed · letzter Lauf: HH:mm" — live aus `runtime/queue/`. - **Tab „Historie":** `DataGrid` über done-Items, neueste zuerst. Spalten: Zeit (`received_at`), Typ, Text-Snippet, **Ziel-Daily-Note** (aus erweitertem Item), Status. Zeilenklick → Detail (voller Text + geschriebener Eintrag). - **Tab „Log":** letzte N Zeilen aus `runtime/logs/` (process-Log + service.log), Auto-Refresh, Monospace. - **Buttons:** *Ingest* · *Process* · *Both* → `python -m journal_bot ` als Subprozess, stdout/stderr in den Log-Tab, Buttons disabled während Lauf, Ergebnis als Statusmeldung. **Datenzugriff:** - `RuntimeReader`: liest `done/`, `pending/`, `failed/`, `logs/`. `FileSystemWatcher` auf `runtime/queue/` + Polling-Fallback alle paar Sekunden → Live-Update. - `BotRunner`: kapselt Subprozess-Start (Python-Pfad + WorkingDir aus `appsettings.json`). - `QueueItem.cs`: C#-DTO, spiegelt das Python-Pydantic-Modell. **Config:** eigene `appsettings.json` neben der Tray-EXE (`PythonExe`, `WorkingDir`, `RuntimePath`, `ProcessTimerMinutes`). **Auto-Process-Timer:** standardmäßig **aus** (`ProcessTimerMinutes` = 0/null). `process` schlägt ohne laufendes LM Studio fehl, daher manueller Trigger als Default. Kann per Config aktiviert werden. ## Komponente 3: Python-Änderungen (erweiterte Historie) Damit die GUI Schreibziel + geschriebenen Eintrag zeigt: **`QueueItem`** (`queue.py`) bekommt optionale Result-Felder, von `process` nach erfolgreichem Schreiben befüllt: ```python target_path: Optional[str] = None # z.B. "05 Daily Notes/2026-06-15.md" written_entry: Optional[str] = None # der tatsächlich geschriebene Markdown-Eintrag processed_at: Optional[str] = None # ISO 8601 ``` Alle optional → keine bestehenden Tests brechen, alte done-Items ohne Felder bleiben lesbar. **`process.py`:** vor `queue.complete(item)` die drei Felder setzen (target_path + entry aus VaultWriter-Aufruf / ProcessorOutput, processed_at = now). **`queue.py` → `complete`:** analog zu `fail` umbauen — aktualisiertes Item-JSON atomar (`​.tmp + replace`) in `done/` schreiben, dann `working/`-Quelle löschen. Erhält die Crash-Safety. (Bisher reines `rename`, das die Result-Info nicht persistieren würde.) **Tests (TDD):** bestehende `complete`-Tests anpassen; neuer Test: nach `process_once` trägt das done-Item `target_path`/`written_entry`/`processed_at`. Erst failing test, dann Umbau. ## Repo-Layout ``` tools/ JournalBot.sln JournalBot.Service/ Worker.cs # PeriodicTimer → python ingest Program.cs # Host + UseWindowsService appsettings.json # PythonExe, WorkingDir, IntervalMinutes JournalBot.Service.csproj JournalBot.Tray/ App.xaml / App.xaml.cs MainWindow.xaml / .cs # Tabs Historie|Log, Status-Banner, Buttons TrayIcon.xaml # NotifyIcon + Kontextmenü Services/ RuntimeReader.cs # liest done/pending/failed/logs, FileSystemWatcher BotRunner.cs # Subprozess python -m journal_bot QueueItem.cs # C#-DTO, spiegelt Python-Pydantic-Modell appsettings.json JournalBot.Tray.csproj scripts/ install-service.ps1 # New-Service / sc.exe, JournalBotService install-tray.ps1 # Autostart @logon (Run-Key oder Scheduler) (install-task.ps1 → entfällt / ersetzt) ``` ## Build & Deployment **Build:** `dotnet publish -c Release -r win-x64 --self-contained` → zwei EXEs, kein .NET-Runtime-Install nötig. **Deployment-Flow:** 1. `.env` füllen (wie gehabt). 2. `dotnet publish` beider Projekte. 3. `install-service.ps1` (Admin) → registriert + startet `JournalBotService` (ingest alle 15 min). 4. `install-tray.ps1` → Tray-App @logon. 5. LM Studio starten, dann in der Tray-GUI `process`/`both` triggern. **Voraussetzung:** .NET SDK installiert (`dotnet --version` zu Beginn prüfen). ## Testing - **Python:** TDD, `pytest` muss grün bleiben (38 Tests + neue). - **.NET:** `RuntimeReader` + `BotRunner` mit xUnit (JSON-Parsing, Pfadlogik). GUI-XAML nicht unit-getestet → manueller Smoke-Test. Service-Timer-Loop manuell verifiziert (`BotRunner` mockbar). ## Doku CLAUDE.md um Abschnitt „Service + Tray (tools/)" ergänzen. ## Offene Fragen - `.env`-Auflösung relativ zum CWD oder absolut? (Beim Bauen des Service verifizieren.) - Autostart der Tray-App: Registry Run-Key vs. Task Scheduler @logon — beim Implementieren festlegen (Run-Key simpler, Scheduler robuster). - `.NET` SDK-Version vorhanden? (`dotnet --version` zu Beginn.)