Files
ClaudeDo/docs/superpowers/specs/2026-08-27-ticketsystem-integration-design.md
T

9.7 KiB
Raw Blame History

Ticketsystem-Anbindung (Bandel TicketSystem)

Datum: 2026-08-27 Status: Design freigegeben, Umsetzung offen

Ziel

Tickets aus dem hauseigenen Ticketsystem (Bandel.APIs) pro Projekt in eine ClaudeDo-Liste importieren und den Ticket-Status automatisch nachziehen, während der Task durch die Queue läuft.

Das Feature ist vollständig optional: ohne konfigurierte Base-URL passiert nichts — kein Netzwerkverkehr, kein Hook, kein sichtbarer Menüeintrag.

Nicht-Ziele (bewusst weggelassen)

Weggelassen Wann nachrüsten
Polling / Hintergrund-Sync Wenn der manuelle Import spürbar nervt
Zwei-Wege-Sync (Ticket-Änderung → Task) Wenn Titel/Beschreibung real auseinanderlaufen
Jira-Provider + ITicketProvider-Interface Wenn Jira tatsächlich kommt — Interface-Extract ist ein Rider-Refactor von 30 s
Update bereits importierter Tickets Würde Task-Notizen überschreiben; erst mit klarer Merge-Regel
Kommentar ans Ticket beim Merge Wenn die Rückverfolgbarkeit fehlt

Der einzige Vorgriff auf Jira ist das Provider-Präfix in TaskEntity.TicketRef (bandel:1234). Das kostet nichts und erspart später eine Migration.

Externe API

Base-URL: http://bandelapis.fb-tuning.local Auth: derselbe Personal Access Token (tsp_…), den auch der Ticket-MCP nutzt. Die relevanten Endpunkte akzeptieren [Authorize(AuthenticationSchemes = "Bearer,Pat")].

Zweck Endpoint Scope
Token-Inhaber ermitteln GET /api/ticketsystem/pat/me — (keine Scope-Policy)
Projekte je Abteilung GET /api/Navigation/sidebar pat:projects:read
Board eines Projekts GET /api/Board/project/{projectId} pat:board:read
Status setzen PATCH /api/Ticket/{id}/status pat:tickets:write

Antworten sind in BandelApiResponse<T> gewrappt ({ Success, Data, Message, ErrorCode }). Das Board liefert GetTicketSummaryDto mit u. a. Id, Title, Description, StatusId, StatusName, AssigneeID, AssigneeName.

Ticket-Status: 0 Keine, 1 Offen, 2 InBearbeitung, 3 Fertig, 4 Archiviert.

Nicht verifiziert: Die API läuft auf srv-04b, gegen den hier nicht geprobt wurde. Die Endpunkte und DTO-Felder stammen aus dem Quellcode von Bandel.APIs, nicht aus einem Live-Aufruf. Der erste manuelle Smoke-Test muss die tatsächlichen Response-Shapes bestätigen (siehe „Manuelle Verifikation").

Konfiguration

Global

  • AppSettingsEntity.TicketApiBaseUrl (string?, Spalte ticket_api_base_url). Null/leer = Feature aus.
  • PAT: DPAPI-verschlüsselt in ~/.claudeDo/ticket.pat, CurrentUser-Scope. OnlineTokenStore macht das bereits exakt so — ihm fehlt nur ein Dateiname-Parameter im parameterlosen Ctor. Der wird zum optionalen Argument, die Klasse wandert nach ClaudeDo.Worker/ (raus aus Online/) und wird von beiden Features genutzt. Kein neuer Krypto-Code.

Der Token landet nie in worker.config.json und nie in der DB.

Pro Liste

  • ListConfigEntity.TicketProjectId (int?, Spalte ticket_project_id). Null = Liste ist nicht mit einem Ticket-Projekt verknüpft.

⚠️ Verbatim-Copy-Falle: ListRepository.SetConfigAsync überschreibt jede Spalte mit dem übergebenen Entity. Jeder Writer (set_list_config MCP-Tool, Listen-Settings-Modal) muss TicketProjectId mitführen, sonst setzt der nächste fremde Schreibvorgang die Verknüpfung still zurück — derselbe Fehler, der SessionSkills schon einmal getroffen hat.

Datenmodell

  • TaskEntity.TicketRef (string?, Spalte ticket_ref) — Format <provider>:<id>, aktuell immer bandel:<ticketId>. Kein Index nötig; der Import filtert über die Liste, nicht global.
  • ListConfigEntity.TicketProjectId (int?)
  • AppSettingsEntity.TicketApiBaseUrl (string?)

Eine EF-Migration mit drei Spalten über drei Tabellen.

⚠️ Migrations-Kollision: Parallel entstandene Migrationen vom selben Elternknoten löschen sich bei einem SQLite-Table-Rebuild gegenseitig die Spalten weg, und die Tests laufen mit EnsureCreated und merken es nicht. Diese Migration entsteht als einzelne, nachdem alle parallelen Tasks gemergt sind — nicht in einem Nebenläufer.

Import: „Update from Ticketsystem"

Einstieg: Kontextmenü der Liste in ListsIslandView.axaml, direkt über „Einstellungen". Sichtbar nur, wenn Base-URL gesetzt und die Liste ein TicketProjectId hat.

Ablauf (Worker, über eine neue Hub-Methode ImportTicketsAsync(listId)):

  1. GET /api/ticketsystem/pat/meUserName des Token-Inhabers.
  2. GET /api/Board/project/{TicketProjectId} → Board-Items.
  3. Filtern auf StatusId == 1 (Offen) und AssigneeName == UserName (Ordinal, case-insensitive). Alles andere wird ignoriert.
  4. Bereits vorhandene TicketRef-Werte der Liste laden; Treffer überspringen.
  5. Pro verbleibendem Ticket ein Task via TaskRepository.AddAsync:
    • Status = Idle, ListId = die Liste
    • Title = Ticket-Titel
    • Description = Ticket-Beschreibung + Leerzeile + Ticket #<id>
    • TicketRef = "bandel:<id>"
    • CreatedBy = "ticketsystem"
  6. Ergebnis als Footer-Meldung: „12 Tickets geprüft, 3 neue Tasks."

Der Import erzeugt nur Idle-Tasks — er queued nichts. Ob und wann etwas läuft, bleibt eine bewusste Nutzeraktion.

Warum Board-Filter statt GET /api/Board/global/{userId}: Das globale Board wählt Items über Zuweisung oder Reporter oder PersonInCharge aus. Das ist breiter als „mir zugewiesen". Der Namensvergleich ist bewusst gewählt (/pat/me liefert keine UserId, und Benutzernamen ändern sich hier nicht).

Write-back: Hook in TaskStateService

TaskStateService ist der einzige Ort im System, der Status schreibt. Ein Hook dort deckt Queue-Picker, Merge/Approve, Cancel, Reset und beide MCP-Oberflächen gleichzeitig ab — jede andere Stelle wäre ein Teilabdeckungs-Fix.

Task-Status Ticket-Status
Running, WaitingForReview 2 InBearbeitung
Done 3 Fertig
alles andere (Idle, Queued, WaitingForChildren, Failed, Cancelled) kein Schreiben

1 Offen wird nie geschrieben: das ist der Eingangszustand. Ein fehlgeschlagener oder abgebrochener Task lässt das Ticket bewusst auf InBearbeitung stehen — die Arbeit ist angefangen, nicht zurückgegeben.

Auslösebedingungen (alle müssen gelten, sonst passiert nichts):

  • task.TicketRef ist gesetzt und beginnt mit bandel:
  • TicketApiBaseUrl ist konfiguriert und ein PAT liegt vor
  • der Zielstatus unterscheidet sich vom letzten geschriebenen (kein redundanter PATCH bei Running → WaitingForReview, beide mappen auf 2)

Der Aufruf ist fire-and-forget mit try/catch: ein Fehler wird als Warnung geloggt (landet über BroadcastLogSink im Footer-Log-Strip) und beendet den Statuswechsel nie. Das Ticketsystem darf die Queue nicht anhalten können.

UI

Settings-Modal — neuer Tab „Ticketsystem"

Unter „Erweitert", wie der Online-Inbox-Tab (SettingsModalView.axaml, ErweitertCategories):

  • Base-URL (Textfeld)
  • Personal Access Token (maskiert; gespeichert = Anzeige „gesetzt", nicht der Wert)
  • Button „Verbindung testen" → GET /api/ticketsystem/pat/me, zeigt Benutzername und die gewährten Scopes an. Fehlt pat:board:read oder pat:tickets:write, wird das als Hinweis ausgegeben — das ist der häufigste Konfigurationsfehler.

Neues TicketSettingsTabViewModel neben OnlineInboxSettingsViewModel, gleiches Muster (LoadAsync, IsBusy, StatusMessage).

Listen-Settings-Modal — Projektzuordnung

Ein Dropdown „Ticket-Projekt" mit den Projekten aus GET /api/Navigation/sidebar, gruppiert nach Abteilung, plus „— keins —". Lädt beim Öffnen des Modals, nur wenn eine Base-URL konfiguriert ist; sonst ist das Feld ausgeblendet.

Lokalisierung

Alle neuen Strings in locales/en.json und locales/de.json. Die Parität wird von Localization.Tests erzwungen — beide Dateien müssen zusätzlich einzeln als JSON parsebar bleiben, ein kaputtes de.json wird sonst still verschluckt.

Fehlerbehandlung

Fall Verhalten
Base-URL nicht gesetzt Feature komplett inaktiv, Menüeintrag versteckt
PAT fehlt/ungültig (401) Import bricht mit Footer-Fehler ab; Write-back loggt Warnung
Scope fehlt (403) Wie 401, Meldung nennt den fehlenden Scope
API nicht erreichbar Timeout 10 s, Footer-Fehler, kein Retry
Write-back schlägt fehl Warnung im Log, Task-Statuswechsel geht durch
Ticket im Ticketsystem gelöscht PATCH liefert 404 → Warnung, TicketRef bleibt stehen

Kein catch {} ohne Meldung — Nutzeraktionen, die scheitern, erscheinen im Footer-Strip.

Tests

Alle mit gefaktem HttpMessageHandler, kein echter Netzwerkaufruf, kein echtes claude:

  • Status-Mapping: jeder TaskStatus → erwarteter Ticket-Status bzw. „kein Aufruf"
  • Kein redundanter PATCH bei Running → WaitingForReview
  • Kein Aufruf ohne TicketRef, ohne Base-URL, ohne PAT
  • Write-back-Fehler (401/404/Timeout) lässt den Statuswechsel durchgehen
  • Import: filtert korrekt auf Assignee + StatusId == 1
  • Import: überspringt bereits importierte Tickets, legt keine Duplikate an
  • SetConfigAsync erhält TicketProjectId, wenn ein anderer Writer die Config anfasst
  • Locale-Parität en/de

Manuelle Verifikation (nicht automatisierbar)

  1. PAT in den Settings hinterlegen, „Verbindung testen" → korrekter Benutzername + Scopes.
  2. Liste mit Projekt 393 Bandel.LagerApp verknüpfen, „Update from Ticketsystem" → nur eigene, offene Tickets erscheinen als Tasks.
  3. Einen Task laufen lassen → Ticket steht im Ticketsystem auf InBearbeitung.
  4. Approve/Merge → Ticket steht auf Fertig.
  5. Base-URL leeren → Menüeintrag verschwindet, keine Netzwerkaufrufe mehr.

Punkte 14 bestätigen zugleich die aus dem Quellcode abgeleiteten Response-Shapes.