8.8 KiB
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:
- Windows Service holt Telegram-Nachrichten alle 15 Minuten selbstständig ab (
ingest). - Tray-GUI (WPF) zeigt die Historie, Queue-Status und Logs und triggert
ingest/process/bothmanuell.
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 alsLocalSystemauch 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: startetpython.exe -m journal_bot ingestals Subprozess, wartet aufs Ende, loggt Exit-Code + stdout/stderr.- Konfiguration via
appsettings.jsonneben der EXE:PythonExe(Pfad zu.venv\Scripts\python.exe),WorkingDir(Projekt-Root),IntervalMinutes(Default 15). - Kontext:
LocalSystem.ingestbraucht nur Telegram + Dateisystem, kein LM Studio, kein User-Profil.WorkingDirwird 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-NameJournalBotService.
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 <cmd>als Subprozess, stdout/stderr in den Log-Tab, Buttons disabled während Lauf, Ergebnis als Statusmeldung.
Datenzugriff:
RuntimeReader: liestdone/,pending/,failed/,logs/.FileSystemWatcheraufruntime/queue/+ Polling-Fallback alle paar Sekunden → Live-Update.BotRunner: kapselt Subprozess-Start (Python-Pfad + WorkingDir ausappsettings.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:
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 <cmd>
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:
.envfüllen (wie gehabt).dotnet publishbeider Projekte.install-service.ps1(Admin) → registriert + startetJournalBotService(ingest alle 15 min).install-tray.ps1→ Tray-App @logon.- LM Studio starten, dann in der Tray-GUI
process/bothtriggern.
Voraussetzung: .NET SDK installiert (dotnet --version zu Beginn prüfen).
Testing
- Python: TDD,
pytestmuss grün bleiben (38 Tests + neue). - .NET:
RuntimeReader+BotRunnermit xUnit (JSON-Parsing, Pfadlogik). GUI-XAML nicht unit-getestet → manueller Smoke-Test. Service-Timer-Loop manuell verifiziert (BotRunnermockbar).
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).
.NETSDK-Version vorhanden? (dotnet --versionzu Beginn.)