157 lines
8.8 KiB
Markdown
157 lines
8.8 KiB
Markdown
# 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 <cmd>` 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 <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:**
|
||
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.)
|