# Ticket-Import über alle Projekte + Tagesplanung — Design **Datum:** 2026-10-01 · **Status:** abgenommen ## Ziel Ein Lauf holt **alle** Mika zugewiesenen, offenen Ticket-Items (Tickets, Tasks, Subtasks) aus dem Bandel-Ticketsystem, lässt Claude jedes Item nach **Inhalt** der passenden ClaudeDo-Liste zuordnen und plant danach den Tag (bestehender FillMyDay-Lauf). Auslösbar per Button und als Prime-Schedule. ## Warum nicht der bestehende Import `TicketImportService.ImportAsync` arbeitet pro Liste über die feste Verknüpfung `ListConfigEntity.TicketProjectId` und nur für Board-Items vom Typ Ticket. Live-Stand 2026-10-01: - 6 von 10 offenen, zugewiesenen Items sind Tasks/Subtasks (z.B. 595/596/598/668 unter „Lager Technik Plugin“) — der heutige Import sieht sie nie. - Projekt „Lager“ (493) enthält Items für mehrere Repos (Hub Lagertechnik, SorterController) und ist mit zwei Listen verknüpft. Projekt→Liste ist die falsche Zuordnungsachse; Inhalt ist die richtige. - DueDate ist fast überall null, Priorität fast überall „Niedrig“ — was heute dran ist, entscheidet weiterhin FillMyDay, nicht die Ticketdaten. ## Entscheidungen | Frage | Entscheidung | |---|---| | Zuordnung | Claude nach Inhalt (Titel, Parent, Projekt). Projekt-Verknüpfung einer Liste ist nur ein Hinweis. | | Granularität | Flach: jedes zugewiesene Ticket/Task/Subtask wird ein eigener ClaudeDo-Task. Parent steht in der Beschreibung. Keine Hierarchie. | | Ablauf | Ein Lauf, zwei Schritte: Zuordnungs-Lauf, danach FillMyDay — im selben `PrimeRunner`-Gate. | | Kein Treffer | Item geht in die **Fallback-Liste** (App-Setting). Signal für Mika: dafür fehlt eine Liste. Ist keine gesetzt → übersprungen und in der Zusammenfassung genannt. | | Fallback anwenden | Der **Worker**, nicht Claude: `import_ticket` ohne `list` → Fallback-Liste. | ## Komponenten ### 1. Datenquelle (`Worker/Tickets/`) **Sweep über alle Projekt-Boards** — nur Routen, die `BandelTicketClient` schon hat und die seit 2026-09-01 live verifiziert sind: `GET /api/Navigation/sidebar` (alle sichtbaren Projekte) → pro Projekt `GET /api/Board/project/{id}`. Filter, `statusId ∈ {1, 2}`: - Ticket (Board-Item): `ticketAssignees[].assigneeName == userName` - Task (`tasks[]` am Board-Item): **`ticketTaskAssignees[].assigneeName`** == userName — unabhängig davon, wem das Parent-Ticket gehört (730/732 hängen an 728 von marcel.sievers). Projekt-Titel kommt aus der Sidebar, Parent aus dem umschließenden Board-Item. `TicketBoardItem` bekommt `Tasks` (`id`, `title`, `statusId`, `hasChildren`, `ticketTaskAssignees`). **Live-Befund 2026-10-01** (7 Projekte, ~1,2 s): exakt die 10 offenen Items, die das Ticket-MCP `list_assigned_items` liefert — 665, 593 (+595/596/598/668), 1014, 1016, 1017, 730, 732. Verworfen wurde `GET /api/board/global/{userId}`: liefert kein Projekt, braucht eine userId, die `/pat/me` nicht hergibt (`/users/username/{u}` = 401 mit PAT), und ist **nicht** auf den Token-Inhaber beschränkt (`global/22` lieferte 147 fremde Items). Subtasks (Ebene 3): live existiert keiner (`hasChildren` überall false). Board-Tasks mit `hasChildren = true` werden übersprungen. **Offen, manuell zu prüfen:** `PATCH /api/Ticket/{id}/status` auf einem **Task** (nicht Ticket) — nicht live getestet, weil es ein Schreibzugriff aufs Firmensystem ist. Laut Bandel.APIs-Breadcrumb gleiche Tabelle, Statusrecht u.a. für Assignees. Scheitert es, loggt `TicketStatusSync` nur eine Warnung (wirft nie) → beim ersten echten Lauf im Worker-Log prüfen. Details pro Item für die Beschreibung: `GET /api/ticket/{id}` (live: liefert `description`, `parentTicketID`). ### 2. `TicketImportService` - `GetCandidatesAsync(ct)` → zugewiesene offene Items, deren `bandel:` noch **in keiner Liste** als `TicketRef` existiert, plus alle User-Listen (Id, Name, WorkingDir, verknüpftes Ticket-Projekt) als Zuordnungskontext. - `ImportItemAsync(itemId, listId?, ct)` → lädt Details via `GET /api/ticket/{id}` (Beschreibung, HTML→Markdown über das bestehende `HtmlToMarkdown`), legt einen `Idle`-Task an: `TicketRef = bandel:`, `CreatedBy = "ticketsystem"`, Beschreibung endet mit `Ticket #` und bei Kind-Items `Teil von # `. `listId == null` → `AppSettings.TicketFallbackListId`; ist die auch null → kein Task, Ergebnis meldet „übersprungen“. Bereits importiert (global) → No-op mit entsprechender Meldung. - **Fix im bestehenden `ImportAsync`:** Dedupe über `TicketRef` **listenübergreifend** statt nur `t.ListId == listId` — sonst legt „Aus Ticketsystem aktualisieren“ ein bereits global zugeordnetes Item ein zweites Mal an. ### 3. MCP-Tools (External-MCP, neue `External/TicketMcpTools`) - `get_ticket_import_candidates()` → `{ available, items[], lists[], fallbackList }`. `available = false` mit Grund, wenn das Ticketsystem nicht eingerichtet ist. - `import_ticket(ticketId, list = null)` → Bestätigungs-Record (`created`, `taskNumber`, `listName`, `usedFallback`, `skippedReason`). `list` akzeptiert Name oder Id. Konventionen aus `src/ClaudeDo.Worker/CLAUDE.md` gelten (C#-Default für optionale Parameter, kein nacktes `Task`-Return, Tool-Doku nach `McpToolDocs`). ### 4. Ablauf (`Worker/Prime/`) - `PrimeActionKind.ImportTickets`, DB-Wert `import_tickets` (Converter in `PrimeScheduleEntityConfiguration`). - `TicketImportPrompt` (analog `DailyPrepPrompt`): Prompt über `PromptFiles` (`PromptKind.TicketImport`), Args `--allowedTools` nur die zwei Tools, `--permission-mode acceptEdits`. Prompt-Kern: Kandidaten holen, jedes Item der inhaltlich passenden Liste zuordnen (Projekt-Verknüpfung = Hinweis), bei Unsicherheit `list` weglassen, am Ende Zusammenfassung (importiert pro Liste, Fallback, übersprungen). - `PrimeRunner.RunImportTicketsAsync`: Log einmal leeren, `PrepStarted`, Zuordnungs-Lauf, dann den FillMyDay-Teil (bestehende Logik ohne erneutes Leeren/`PrepStarted`), `PrepFinished`. Schlägt der Import fehl, läuft FillMyDay trotzdem (bestehende Tasks sind weiter planbar); das Outcome meldet den Importfehler. - Ist das Ticketsystem nicht eingerichtet, fällt der Lauf auf reines FillMyDay zurück. ### 5. Hub + UI - `WorkerHub.RunTicketImportNow()` (synthetischer Schedule, Kind fix `ImportTickets`, wie `RunDailyPrepNow`); `IWorkerClient`/`WorkerClient` + Test-Fakes nachziehen. - Prep-Panel (`DetailsIslandView`, neben „Plan day“): Button „Tickets + Tag planen“, nur sichtbar wenn das Ticketsystem eingerichtet ist; gleiche Enable-Logik wie `PlanDayCommand`. - Prime-Tab: neuer Aktionstyp wählbar. - Settings → Ticketsystem: Dropdown „Fallback-Liste“ (User-Listen + „keine“). - `AppSettingsEntity.TicketFallbackListId` (nullable) + EF-Migration. Locale-Keys en + de. ## Tests - `GetCandidatesAsync`: Status-Filter, Assignee-Filter über `ticketAssignees`, globale Dedupe. - `ImportItemAsync`: explizite Liste, Fallback, keine Fallback-Liste → übersprungen, Duplikat. - `ImportAsync`: Item bereits in anderer Liste → nicht erneut angelegt. - `PrimeRunner`: `ImportTickets` → zwei Claude-Läufe in Reihenfolge, ein Gate, Import-Fehler blockiert FillMyDay nicht. - Fake-Client/Fake-Claude, **kein** echtes Claude. Live-Verifikation (Schritt 1) und Sichtprüfung der UI sind manuelle Punkte → `docs/open.md`. ## Bewusst nicht drin - Rückabgleich „Ticket im Ticketsystem erledigt, Task in ClaudeDo noch offen“. - Ticket-Hierarchie als ClaudeDo-Parent/Subtasks. - Aktualisieren bereits importierter Items (würde Notizen überschreiben — gleiche Regel wie heute).