10 KiB
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
- Base-URL:
ticket_api_base_urlin~/.claudeDo/worker.config.json. Null/leer = Feature aus.Der Plan weicht hier bewusst vom ursprünglichen Entwurf ab (der eine Spalte in
AppSettingsEntityvorsah):WorkerConfig.SaveKeykann das read-modify-write bereits generisch,claude_binist der Präzedenzfall für ein UI-schreibbares Feld in dieser Datei, und weilWorkerConfigein DI-Singleton ist, kommt der Hook inTaskStateServiceohne DB-Abfrage aus. Spart zugleich eine Migrationsspalte. - PAT: DPAPI-verschlüsselt in
~/.claudeDo/ticket.pat, CurrentUser-Scope.OnlineTokenStoremacht das bereits exakt so — ihm fehlt nur ein Dateiname-Parameter im parameterlosen Ctor. Der wird zum optionalen Argument, die Klasse wandert nachClaudeDo.Worker/(raus ausOnline/) 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?, Spalteticket_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_configMCP-Tool, Listen-Settings-Modal) mussTicketProjectIdmitführen, sonst setzt der nächste fremde Schreibvorgang die Verknüpfung still zurück — derselbe Fehler, derSessionSkillsschon einmal getroffen hat.
Datenmodell
TaskEntity.TicketRef(string?, Spalteticket_ref) — Format<provider>:<id>, aktuell immerbandel:<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
EnsureCreatedund 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)):
GET /api/ticketsystem/pat/me→UserNamedes Token-Inhabers.GET /api/Board/project/{TicketProjectId}→ Board-Items.- Filtern auf
StatusId == 1(Offen) undAssigneeName == UserName(Ordinal, case-insensitive). Alles andere wird ignoriert. - Bereits vorhandene
TicketRef-Werte der Liste laden; Treffer überspringen. - Pro verbleibendem Ticket ein Task via
TaskRepository.AddAsync:Status = Idle,ListId= die ListeTitle= Ticket-TitelDescription= Ticket-Beschreibung + Leerzeile +Ticket #<id>TicketRef = "bandel:<id>"CreatedBy = "ticketsystem"
- 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.TicketRefist gesetzt und beginnt mitbandel:TicketApiBaseUrlist konfiguriert und ein PAT liegt vor- der Zielstatus unterscheidet sich vom letzten geschriebenen (kein redundanter PATCH bei
Running → WaitingForReview, beide mappen auf2)
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. Fehltpat:board:readoderpat: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
SetConfigAsyncerhältTicketProjectId, wenn ein anderer Writer die Config anfasst- Locale-Parität en/de
Manuelle Verifikation (nicht automatisierbar)
- PAT in den Settings hinterlegen, „Verbindung testen" → korrekter Benutzername + Scopes.
- Liste mit Projekt
393 Bandel.LagerAppverknüpfen, „Update from Ticketsystem" → nur eigene, offene Tickets erscheinen als Tasks. - Einen Task laufen lassen → Ticket steht im Ticketsystem auf
InBearbeitung. - Approve/Merge → Ticket steht auf
Fertig. - Base-URL leeren → Menüeintrag verschwindet, keine Netzwerkaufrufe mehr.
Punkte 1–4 bestätigen zugleich die aus dem Quellcode abgeleiteten Response-Shapes.