journal-bot/docs/superpowers/specs/2026-06-16-service-tray-gui...

8.8 KiB
Raw Blame History

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 : BackgroundServicePeriodicTimer(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 · Bothpython -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:

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.pycomplete: 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.)