# Redmine Feature -- Ticket-Topologie, Browser, AI-Tools, Workflow-Nodes ## Beschreibung und Kontext Wir nutzen in mehreren Software-Projekten **Redmine** als Ticketsystem (Userstories, Acceptance Criteria, Features, plus deren Beziehungen). Aus dem MARS/SSS-Pilot (`pamocreate/projects/valueon/sss/project_mars/redmine-sync/`) wissen wir, dass die Redmine-REST-API trägt: lesen, anlegen, ändern, Beziehungen verwalten. Bisher passierte das per Skript. Wir wollen Redmine als **erstklassiges Feature in PORTA** betreiben, sodass User die Tickets direkt im Workspace browsen, editieren, automatisieren und durch AI-Agents/Workflows verarbeiten können. **Business-Treiber:** Verkürzte Schleife zwischen Konzeption (KI-gestützt im Workspace), Anforderungs-Pflege (Userstories/Acc.Criteria/Features) und Backlog. Mehrere Projekte (myABI/SSS, weitere LO-Mandanten) sollen gleichzeitig an unterschiedliche Redmine-Instanzen/-Projekte angebunden werden -- daher **Konfiguration pro Feature-Instanz**. **Risiko bei Nicht-Umsetzung:** Wir bleiben beim Sync-via-Markdown-Skript-Workflow; jede neue Auswertung (z. B. Topologie, Beziehungs-Filter) braucht ein Einzelskript; Agents haben keinen Lese-/Schreibzugriff auf Tickets. **Pilot-Referenz:** `pamocreate/projects/valueon/sss/project_mars/redmine-sync/code/_redmineClient.py` (REST-Wrapper, idempotente Reads/Writes), `inventoryFeatures.py` (Beziehungs-Parsing), `README.md` (Trackers, Custom Fields, Sprint-Limits). ## Fokus und kritische Details - **Userstory = Wurzel, Orphans separat.** Im Browser ist die Wurzel jedes Trees eine **User Story**; alle anderen Tracker (Feature, Acc.Criteria, Bug, Task) erscheinen als Nachfahren entlang aktiver Relation-Typen (richtungsunabhängig). Tickets, die an **keiner** US hängen, werden unter einem virtuellen **Orphan**-Wurzelknoten gesammelt (= "Tickets ohne Verbindung zu einer User Story") -- so bleiben sie auffindbar und sind als KPI ausweisbar. - **Wurzel-Tracker explizit aus der Config.** Kein Heuristik-Fallback. Im `RedmineInstanceConfig` steht `rootTrackerName` (Default `Userstory`); beim Schema-Fetch wird dieser Name gegen die Tracker-Liste des Projekts case-insensitive aufgelöst. Falls der Tracker nicht existiert, ist `rootTrackerId=None` und das UI zeigt einen Konfig-Fehler. - **Lokaler Mirror für 20k+ Tickets.** Tickets und Relations werden in `poweron_redmine` gespiegelt (`RedmineTicketMirror`, `RedmineRelationMirror`, beide `PowerOnModel` mit Auto-DDL). Stats und Browser lesen ausschliesslich aus dem Mirror. Schreibops gehen an Redmine und upserten **danach** sofort den betroffenen Ticket-Datensatz im Mirror. Eigener Service `serviceRedmineSync` mit per-Instanz `asyncio.Lock`, Routes `POST /api/redmine/{instanceId}/sync?force=` und `GET /sync/status` -- gestartet manuell aus der Settings-Page (Button), später optional zeitgesteuert. - **Statistik = erste Seite des Features.** Eigene Page mit KPI-Tiles (Total / Offen / Geschlossen in Periode / Neu in Periode / Orphan-Count) und Diagrammen via `FormGeneratorReport` (siehe Abschnitt "Statistik-Sektionen"). - **Zeitraum-Auswahl: ausschliesslich `PeriodPicker` (`components/PeriodPicker`).** Keine Custom-Dropdowns. `PeriodValue` (`{preset, fromDate, toDate}`) ist die einzige Quelle der Wahrheit; gesendet wird via `dateFrom`/`dateTo` an das Backend (analog `platform-core/modules/shared/dateRange.py`). Im Stats-Page wird `PeriodPicker` direkt vom `FormGeneratorReport.dateRangeSelector` gerendert; im Browser-Page-Filter-Bar wird er als Standalone-Komponente eingesetzt. Default-Preset für Stats: `last12Months`; für Browser: `lastMonth`. - **Pro Feature-Instanz** eine Verbindung: `redmineBaseUrl`, `apiKey` (encrypted), `projectId`, optional Whitelist-Tracker / Default-Custom-Fields. Mehrere Instanzen pro Mandat möglich. - **API-Key sicher speichern:** Wie andere Connector-Secrets (`apiTokenConfigKey`-Pattern bei Jira) -- verschlüsselt im Mandat-Secret-Store, nie ins Frontend zurückgeben. - **Idempotenz beim Schreiben:** Vor jedem `PUT/POST` aktuellen Stand lesen, nur Diff schreiben (siehe `applyPlan.py`-Pattern). - **Backlogs-Plugin (Sprint).** Redmine-Standard kennt keine Sprint-Zuweisung via REST; das Backlogs-Plugin speichert separat. Read-Only im V1, Hinweis bei Schreibversuchen. - **Custom Fields pro Projekt unterschiedlich.** Wir lesen `/projects/{id}/custom_fields` beim Connect und cachen das Schema pro Instanz; Edit-Form rendert dynamisch (analog `FormGenerator`). - **Workflow-Limits.** `DELETE /issues` ist mit unseren Keys typischerweise verboten (HTTP 403); `Closed` + `resolution_type=invalid` als Soft-Delete. Statuswechsel ausserhalb des erlaubten Workflows liefern `204` ohne Effekt -- nach jedem `PUT` validieren. - **Rate-Limit / Throttle.** Connector mit `asyncio.Semaphore`-basierter Drosselung (Default 5 parallel, 150ms Throttle wie im Pilot). - **Tests im Porta-UI, nicht via pytest-Sandbox.** Verbindungs- und Sync-Tests laufen über die Buttons "Verbindung testen" und "Sync starten" in `RedmineSettingsView`. Keine externen Pytest-Live-Tests, keine ENV-Variablen-Choreographie. Pure Aggregations-/Cache-/Orphan-Logik bleibt offline-testbar mit Snapshot-Fixture (`tests/fixtures/redmineSnapshot.json`). - **i18n:** Alle UI-Texte über `t('Deutscher Klartext')`; Backend-Felder (Statusnamen, Trackernamen, CF-Labels) kommen direkt aus Redmine -- nicht durch `t()` jagen. ### Statistik-Sektionen (V1) -- alle als `FormGeneratorReport`-Sections | Section | `type` | Inhalt | Datenquelle | |---------|--------|--------|-------------| | KPI-Tiles | `kpiGrid` | Total · Offen (% Anteil) · Geschlossen in Periode · Neu in Periode · Orphan-Count | aggregierte Counts | | Status pro Tracker | `horizontalBar` (stacked) | Pro Tracker je ein Balken, Segmente nach Status | Group-by Tracker × Status, Snapshot | | Throughput | `lineChart` (2 Serien: erstellt, geschlossen) | Pro Bucket (Tag/Woche/Monat) | Group-by Bucket auf `created_on` und auf `updated_on` (closed) | | Top Bearbeiter (offen) | `horizontalBar` | Top 6 Personen mit den meisten offenen Tickets | Group-by `assigned_to_id`, `isClosed=false` | | Beziehungs-Verteilung | `pieChart` (donut) | Anteile der Beziehungstypen | Group-by `relation_type` über sichtbare Tickets | | Backlog-Aging | `barChart` | 5 Buckets (<7, 7-30, 30-90, 90-180, >180 Tage) auf `now - updated_on` | offene Tickets nach Alter | Backend liefert für die Stats-Page **einen** Endpoint `GET /api/redmine/{instanceId}/stats?dateFrom=&dateTo=&bucket=&trackers=` und gibt eine `RedmineStatsDto` zurück (alle 6 Sektions-Daten in einem Round-Trip; spart 5 Calls). ### Optionale spätere Sektionen (Phase 2+) MTTR pro Tracker · Sprint-Burnup je `fixedVersion` · Re-Open-Rate · Cumulative Flow Diagram · Bug-Inflow vs. Feature-Inflow. ## Ziel und Nicht-Ziele **Ziel V1:** - Feature-Modul `redmine` mit Konfig pro Instanz, RBAC-Katalog, Navigation-Views. - Backend-Connector `connectorTicketsRedmine.py` (read/edit/write/relations) + `serviceRedmineStats` (Aggregationen für die Statistik-Page). - 3 Frontend-Seiten: **Statistik** (Default), **Browser**, **Config**. - AI-Tools (Toolbox `redmine`, always-on innerhalb der Feature-Instanz): `readRedmineTicket`, `searchRedmineTickets`, `updateRedmineTicket`, `createRedmineTicket`, `addRedmineRelation`, `listRedmineMeta`, `getRedmineStats`. - Workflow-Method `methodRedmine` mit denselben Aktionen für Graph-Editor / Automation. - HTML-Pilot (Mock-JSON, kein Backend) zur UI-Validierung **vor** dem Backend-Bau -- **erledigt** (`local/notes/redmine-pilot/`). **Explizit NICHT (V1):** - Kein Sprint-Management (Backlogs-Plugin schreiben). Nur Anzeige + Hinweis. - Kein Anhang-Upload/-Download (Phase 2). - Kein Watcher-Management, keine Time-Entries. - Kein Bulk-Edit-UI (CLI/Workflow ja, UI später). - Keine Webhook-Subscriptions (Polling-only V1). ## Betroffene Module - **Gateway:** - `modules/connectors/connectorTicketsRedmine.py` (NEU) -- `aiohttp`-basiert, analog `connectorTicketsJira.py`. - `modules/features/redmine/` (NEU): `mainRedmine.py` (Feature-Code, RBAC-Katalog), `routeFeatureRedmine.py` (REST-API), `serviceRedmine.py` (Geschäftslogik, Idempotenz, Caching), `serviceRedmineStats.py` (Aggregationen für Stats-Page, gemeinsam genutzt von Route + AI-Tool), `interfaceFeatureRedmine.py` (Instance-Config-Persistenz, Schema-Cache), `datamodelRedmine.py` (Pydantic: `RedmineInstanceConfig`, `RedmineTicketDto`, `RedmineRelationDto`, `RedmineFieldSchemaDto`, `RedmineStatsDto`). - `modules/workflows/methods/methodRedmine/` (NEU): `methodRedmine.py` + `actions/` (`connectRedmine`, `readTicket`, `searchTickets`, `updateTicket`, `createTicket`, `addRelation`, `getStats`). - `modules/serviceCenter/services/serviceAgent/coreTools/_redmineTools.py` (NEU) bzw. Erweiterung in `registerCore.py` (Toolbox `redmine`, always-on bei vorhandener Feature-Instanz). - `modules/system/registry.py` -- Feature-Discovery erweitert (kein Code-Change wenn Auto-Discover greift, sonst Eintrag). - `modules/shared/dateRange.py` -- bereits vorhanden, **wiederverwenden** (kein Neubau). - **Frontend:** - `pages/views/redmine/RedmineConfigPage.tsx` (NEU) - `pages/views/redmine/RedmineStatsPage.tsx` (NEU) -- *ersetzt* `RedmineTopologyPage`; rendert primär `` mit aktiviertem `dateRangeSelector` (= PeriodPicker), `filters` (Tracker-Multiselect) und `periodSelector` (Granularität day/week/month). - `pages/views/redmine/RedmineBrowserPage.tsx` (NEU) - `components/RedmineTicketTree/` (NEU, im Browser; rendert User-Story-Roots + virtuelle Orphan-Wurzel) - `components/RedmineTicketEditor/` (NEU, dynamische Felder via FormGenerator) - `api/redmineApi.ts` (NEU) -- `getStats({dateFrom, dateTo, bucket, trackers})`, `listTickets({dateFrom, dateTo, ...})`, `getTicket`, `updateTicket`, `createTicket`, `addRelation`, `getMeta`, `testConfig` - `config/pageRegistry.tsx` -- 3 Seiten registrieren (Default-Page = Stats) - **PeriodPicker:** `components/PeriodPicker` -- ohne Änderung wiederverwendet (in Stats indirekt via `FormGeneratorReport.dateRangeSelector`, im Browser direkt). - **DB-Migration:** - **Ja**, eine Tabelle `RedmineInstanceConfig` (FK auf `FeatureInstance`) -- speichert Base-URL, projectId, encrypted apiKey, Default-Filter (incl. `defaultPeriodPreset` als JSON-Snapshot des `PeriodValue`), `rootTrackerId` (default Userstory), Schema-Cache-TTL. Schema-Daten (Trackers, Statuses, CFs) liegen in einer separaten JSON-Spalte als Cache. - **Andere:** - `wiki/b-reference/platform-core/features/redmine.md` (NEU, beim Done) - `wiki/TOPICS.md` Eintrag (beim Done) ## Entscheidungen | Datum | Entscheidung | Begründung | |-------|-------------|------------| | 2026-04-21 | Konfig pro Feature-Instanz (nicht pro User/Mandat) | Mehrere Redmine-Projekte pro Mandat müssen parallel gehen; Connector-Secrets pro Instanz analog Trustee/CommCoach. | | 2026-04-21 | User Story = fixe Tree-Wurzel (Tracker konfigurierbar), Orphans unter virtuellem Wurzelknoten | User-Vorgabe nach Pilot-Review: Userstory-zentrierte Sicht; Orphans dürfen nicht "verschwinden", müssen als KPI sichtbar sein. | | 2026-04-21 | Topologie-Page **gestrichen**, Sicht im Browser integriert | Doppelte Tree-Sicht war redundant; Browser kann beides. | | 2026-04-21 | Statistik-Page als **erste Seite** des Features | User-Vorgabe nach Pilot-Review; soll bei Öffnen des Features ein "State of the Backlog" liefern. | | 2026-04-21 | Diagramme via `FormGeneratorReport` -- KEINE Custom-Charts | Bestehender Standard, Recharts-basiert, in 6+ Pages produktiv (Billing, Trustee, Audit). | | 2026-04-21 | Zeitraum-Auswahl nur via `PeriodPicker` -- KEINE Custom-Dropdowns | User-Vorgabe; einheitliche UX, Presets (`ytd`, `lastMonth`, `last12Months`, `lastN`, `custom`) und Round-trip-Stabilität sind in der Komponente bereits gelöst. | | 2026-04-21 | AI-Tools always-on innerhalb der Feature-Instanz (kein Connection-Gating) | User-Entscheid; analog Trustee-Tools. Authentifizierung läuft via Instance-Config nicht via UserConnection. | | 2026-04-21 | HTML-Pilot mit Mock-JSON aus `tickets-redmine.csv` | Schnellster UI-Feedback-Loop; keine API-Kopplung beim UI-Design. | | 2026-04-21 | Backlogs-Sprint nur Read-Only V1 | REST-API liefert kein verlässliches Schreiben (siehe Pilot README §5). | | 2026-04-21 | DELETE-Operation als "Close + resolution=invalid" | API-Restriktion (HTTP 403); konsistent mit Pilot-Verhalten. | | 2026-04-21 | `RedmineStatsDto` als Roh-Buckets, Section-Mapping im Frontend | Backend bleibt unabhängig vom `FormGeneratorReport`-Schema; saubere Trennung. | | 2026-04-21 | Multi-Instanz analog Trustee-Pattern (Pfad `/redmine/{instanceId}`, Instance-Context-Resolver) | Standard für Mandat-aktivierbare Features; bookmarkbar, RBAC-Scoping einheitlich. | | 2026-04-21 | Tests direkt gegen SSS-Sandbox (`redmine.logobject.ch`, Projekt myABI/SSS) -- keine Mocks/VCR | Realistisches Verhalten inkl. Custom-Field-Set, Workflow-Restriktionen, Beziehungslimits. CI markiert `pytest.mark.live` zum Skippen ohne Netz. | | 2026-04-21 | Stats-Cache TTL 60s + Write-Invalidation ab V1 | "Saubere Lösung von Anfang an"; vermeidet wiederholte Aggregations-Latenz auf grossen Projekten und ist trivial später zu tunen. | ## Umsetzungs-Checkliste ### Phase 0 -- HTML-Pilot (UI-Validierung, ohne Backend) -- **erledigt** - [x] Mock-JSON aus `tickets-redmine.csv` extrahieren (~19 Tickets, alle Tracker + Beziehungstypen + 4 Orphans) - [x] `local/notes/redmine-pilot/index.html` -- Tabs: Statistik (Default), Browser, Config - [x] Statistik-View: KPI-Tiles + 5 SVG-Charts (Status-pro-Tracker, Throughput, Top-Bearbeiter, Beziehungs-Donut, Backlog-Aging) als Vorlage für `FormGeneratorReport` - [x] Browser-View: Tree links (Userstory-Wurzeln + virtueller Orphan-Wurzelknoten, kollabierbar, Multi-Filter), rechts Edit-Form - [x] Config-View: Form für Base-URL, API-Key, Project-ID, Default-Filter, Wurzel-Tracker - [x] User-Review/Feedback-Schleife → UI-Konzept eingefroren ### Phase 1 -- Backend Connector + Feature - [ ] `datamodelRedmine.py` -- `RedmineInstanceConfig`, `RedmineTicketDto`, `RedmineRelationDto`, `RedmineFieldSchemaDto`, `RedmineStatsDto` (mit Substruktur pro Section) - [ ] `connectorTicketsRedmine.py`: `getIssue`, `listIssues` (paginated, `updated_on>=` filter), `updateIssue`, `createIssue`, `addRelation`, `listRelations`, `getProjectMeta` (Trackers/Statuses/CFs), `whoAmI` - [ ] DB-Migration: `redmine_instance_config` Tabelle, FK auf `feature_instance`. Spalten: `base_url`, `project_id`, `api_key_secret_id`, `root_tracker_id`, `default_period_value` (JSONB, `PeriodValue`-Snapshot), `schema_cache` (JSONB), `schema_cached_at` - [ ] `interfaceFeatureRedmine.py` -- Config CRUD (apiKey verschlüsselt), Schema-Cache (Trackers/Statuses/CFs mit TTL, default 24h) - [ ] `serviceRedmine.py` -- Idempotenz-Layer (Read-before-Write), Filter/Pagination, Throttle (Semaphore default 5/150ms) - [ ] `serviceRedmineStats.py` -- Aggregation in 6 Sektionen (`statusByTracker`, `throughput`, `topAssignees`, `relationDistribution`, `backlogAging`, `kpis`); akzeptiert `dateFrom`/`dateTo` (via `dateRange.parseIsoDateRange`) + `bucket` (`day|week|month`) + optional `trackerIds`. Berechnet **Orphan-Count** = Tickets nicht erreichbar von einer User-Story-Wurzel via Relationen jeden Typs. Liefert **Roh-Buckets** im `RedmineStatsDto` (kein `FormGeneratorReport`-Schema). - [ ] `serviceRedmineStatsCache.py` -- TTL-Cache (default 60s) keyed auf `(instanceId, dateFrom, dateTo, bucket, sortedTrackerIds)`. Invalidierung pro Instance bei `update/create/addRelation`-Calls in `serviceRedmine`. - [ ] `mainRedmine.py` -- `FEATURE_CODE="redmine"`, `UI_OBJECTS=["redmineStats", "redmineBrowser", "redmineConfig"]`, `DATA_OBJECTS`, `TEMPLATE_ROLES` - [ ] `routeFeatureRedmine.py` -- `/api/redmine/{instanceId}/...`: - `GET stats?dateFrom=&dateTo=&bucket=&trackers=` → `RedmineStatsDto` - `GET tickets?dateFrom=&dateTo=&trackers=&status=&search=` → `list[RedmineTicketDto]` - `GET tickets/{id}`, `PUT tickets/{id}`, `POST tickets`, `POST tickets/{id}/relations` - `GET meta`, `POST config/test` - [ ] RBAC-Permissions: `redmine.read`, `redmine.write`, `redmine.config` - [ ] Audit-Log Events (`category="redmine"`, actions: `read`, `update`, `create`, `addRelation`, `configChange`) ### Phase 2 -- Frontend - [ ] `RedmineConfigPage` -- Form, Test-Button (`POST config/test`), Schema-Refresh-Button, Wurzel-Tracker-Dropdown, Default-`PeriodPicker` (Snapshot wird in `default_period_value` gespeichert) - [ ] `RedmineStatsPage` -- nutzt `` mit: - `dateRangeSelector={ enabled: true, defaultPresetKind: 'last12Months', direction: 'past', enabledPresets: ['lastMonth', 'thisQuarter', 'lastQuarter', 'last12Months', 'ytd', 'lastN', 'custom'] }` - `periodSelector={ periods: ['day','week','month'], defaultPeriod: 'week' }` (Bucket-Granularität) - `filters=[{ key: 'trackers', label: t('Tracker'), type: 'multiselect', options: meta.trackers }]` - `onFilterChange` → `getStats(...)` → `setSections(...)` (KPI + 5 Charts wie unter "Statistik-Sektionen" definiert) - [ ] `RedmineBrowserPage` -- Split-Layout: TicketTree links + TicketEditor rechts; Save → re-fetch + Re-render. Filter-Bar enthält **`` (standalone)** für Bearbeitungsperiode. - [ ] `RedmineTicketTree` -- collapsible, Multi-Filter (Tracker, Relation-Type, Assignee), Userstory-Wurzeln + **virtueller Orphan-Wurzelknoten** (gestrichelte Pille, zeigt Anzahl Top-Level-Orphans, lässt sich auf-/zuklappen) - [ ] `RedmineTicketEditor` -- dynamisches Form aus `meta.customFields` (FormGenerator), Status-Dropdown, Subject, Description, Assignee, Tracker, Beziehungs-Liste mit Klick-Navigation - [ ] `redmineApi.ts` -- `useApi`-Wrapper für alle Endpoints; `getStats({dateFrom, dateTo, bucket, trackers})` mit Debounce 300ms - [ ] `pageRegistry.tsx` -- 3 Seiten lazy-registriert mit `objectKey`, Default-Page = `redmineStats` - [ ] i18n-Keys gepflegt (`t('Speichern')`, `t('Beziehung hinzufügen')`, `t('Tickets ohne Verbindung zu einer User Story')`, ...) - [ ] Confirm-Dialog beim Verlassen mit ungespeicherten Änderungen ### Phase 3 -- AI-Tools + Workflow-Method - [ ] `_redmineTools.py` -- Tools registriert in Toolbox `redmine` (immer aktiv bei vorhandener Feature-Instanz): - `readRedmineTicket(issueId, includeRelations)` -- readOnly - `searchRedmineTickets(query, tracker, status, dateFrom, dateTo)` -- readOnly - `listRedmineMeta()` -- readOnly (Trackers, Statuses, CFs) - `getRedmineStats(dateFrom, dateTo, bucket, trackers)` -- readOnly, **gleiche Aggregation wie UI** - `updateRedmineTicket(issueId, fields, notes)` -- write - `createRedmineTicket(subject, description, tracker, customFields)` -- write - `addRedmineRelation(fromId, toId, relationType)` -- write - [ ] `toolboxRegistry.py` -- Toolbox-Definition `redmine` mit `featureCode="redmine"` - [ ] `methodRedmine` + Actions analog `methodJira` -- `actionId="redmine."`, `dynamicMode=True`, `FrontendType`-Annotationen - [ ] FlowEditor: Nodes erscheinen automatisch via `ActionToolAdapter` -- visuell prüfen - [ ] Tests: Unit-Tests für Connector (Mock-Server), Integration-Tests gegen einen lokalen Redmine-Sandbox (oder VCR-Cassette) ### Phase 4 -- Doku & Cleanup - [ ] `wiki/b-reference/platform-core/features/redmine.md` neu anlegen (mit Sektion "Statistik-Aggregationen") - [ ] `wiki/TOPICS.md` Eintrag - [ ] Pilot-Skript-Doku (`redmine-sync`) ergänzen: "→ produktiv via PORTA-Feature 'redmine'" - [ ] Plan-Dokument nach `c-work/4-done/` verschieben ## Akzeptanzkriterien | # | Kriterium (Given-When-Then) | Prio | |---|---------------------------|------| | 1 | Given gültige Redmine-Config in Feature-Instanz, When User die Config-Seite öffnet und auf "Verbindung testen" klickt, Then sieht er Username, Project-Name, Anzahl gelisteter Trackers/CFs | must | | 2 | Given Tickets mit `relates`/`parent`/`blocks`-Beziehungen und einer User Story als Wurzel, When User den Browser öffnet, Then sind alle Tickets entweder unter der passenden User Story eingerückt **oder** unter dem virtuellen Orphan-Wurzelknoten gelistet -- kein Ticket geht verloren | must | | 3 | Given ein Ticket im Browser angeklickt, When User die Description ändert und auf Speichern klickt, Then wird `PUT /issues/{id}` ausgeführt UND nach Re-Fetch die geänderte Description im UI sichtbar | must | | 4 | Given die Statistik-Page geöffnet, When User im **PeriodPicker** den Preset "letzter Monat" wählt, Then aktualisieren sich KPIs UND alle 5 Diagramme; im Throughput-Chart sind nur Buckets innerhalb dieser Periode sichtbar | must | | 5 | Given AI-Agent im Workspace mit aktiver Redmine-Feature-Instanz, When User "Lies Ticket #99526 und fasse es zusammen" eingibt, Then ruft Agent `readRedmineTicket` auf und liefert die Zusammenfassung | must | | 6 | Given AI-Agent, When User "Setze Status von #99526 auf In Progress" sagt, Then ruft Agent `updateRedmineTicket` mit `{status_id: }` auf, und Redmine bestätigt | must | | 7 | Given GraphEditor mit `redmine.searchTickets` → `redmine.updateTicket`-Workflow, When der Workflow ausgeführt wird, Then werden gefundene Tickets erwartungsgemäss aktualisiert | should | | 8 | Given Versuch `DELETE /issues/{id}` aus dem UI, When ausgelöst, Then führt das Backend stattdessen Close + `resolution_type=invalid` aus und meldet das im Toast | should | | 9 | Given fehlerhafte Redmine-Antwort (HTTP 422), When ein Update fehlschlägt, Then sieht User die fehlerhaften Felder farblich markiert mit Original-Errormeldung | must | | 10 | Given zwei Mandate mit unterschiedlichen Redmine-Configs, When ein User der App auf Tickets von Mandant B zugreift, Then bekommt er nur Tickets aus dessen Project-ID (RBAC + Instance-Scoping) | must | | 11 | Given Stats-Page mit Tracker-Filter "nur Bug+Task", When neu geladen, Then enthält der `getStats`-Call den Parameter `trackers=1,3`, Backend filtert serverseitig, alle 5 Charts zeigen ausschliesslich diese Tracker | must | | 12 | Given AI-Agent, When User "Wieviele Bugs sind in den letzten 30 Tagen entstanden?" fragt, Then ruft Agent `getRedmineStats(dateFrom=now-30d, dateTo=now, trackers=[Bug])` auf und liefert die `kpis.createdInPeriod`-Zahl | should | | 13 | Given Tickets ohne `assigned_to_id`, When auf Statistik-Page geladen, Then erscheint im "Top Bearbeiter"-Chart eine Zeile "(nicht zugewiesen)" mit korrekter Anzahl | should | ## Testplan Alle Connector-/Route-Tests laufen **live** gegen `redmine.logobject.ch`, Projekt myABI/SSS (Test-Tickets in einem Sprint-Bereich, der ausschliesslich für CI verwendet wird). Marker `pytest.mark.live` ermöglicht skippen, wenn `REDMINE_API_KEY`-Env nicht gesetzt ist. Service-/Stats-Tests laufen ohne Netz auf Fixture-Daten (Snapshot der Live-Tickets als JSON, einmalig erzeugt via Helper-Skript). | ID | AC | Art | Automatisiert | Repo-Pfad | Status | |----|----|-----|--------------|-----------|--------| | T1 | 1 | live-integration | ja (`mark.live`) | platform-core/tests/test_connector_redmine_live.py (whoAmI, getProjectMeta, getIssue, listIssues paginated) | pending | | T2 | 1 | api | ja | platform-core/tests/test_route_redmine_config.py | pending | | T3 | 2 | unit | ja | platform-core/tests/test_service_redmine_orphans.py (Tree-Builder + Orphan-Detection auf Fixture-JSON) | pending | | T4 | 3 | live-integration | ja (`mark.live`) | platform-core/tests/test_route_redmine_update_live.py (Read-modify-write Round-trip) | pending | | T5 | 4,11 | unit | ja | platform-core/tests/test_service_redmine_stats.py (Period-Filter, Tracker-Filter, Bucket-Aggregation auf Fixture-JSON) | pending | | T6 | 5,6,12 | integration | ja | platform-core/tests/test_redmine_tools.py (`mark.live` für Write-Tools) | pending | | T7 | 7 | integration | ja | platform-core/tests/test_method_redmine.py | pending | | T8 | 8 | api | ja | platform-core/tests/test_route_redmine_delete.py (Soft-Delete-Mapping + Toast-Message) | pending | | T9 | 9 | ui | manuell | -- | pending | | T10 | 10 | api | ja | platform-core/tests/test_route_redmine_rbac.py | pending | | T11 | 4 | ui | manuell | -- (PeriodPicker-Round-trip in Stats) | pending | | T12 | 13 | unit | ja | platform-core/tests/test_service_redmine_stats.py::test_unassigned_bucket | pending | | T13 | -- | unit | ja | platform-core/tests/test_service_redmine_stats_cache.py (TTL, Hit/Miss, Invalidierung bei Write) | pending | ## Links - Pilot-Code: `pamocreate/projects/valueon/sss/project_mars/redmine-sync/code/` - Pilot-README: `pamocreate/projects/valueon/sss/project_mars/redmine-sync/code/README.md` - HTML-Pilot (Phase 0, **erledigt**): `poweron/local/notes/redmine-pilot/index.html` - Pilot-Doku: `poweron/local/notes/redmine-pilot/README.md` - Redmine REST-API: - Vergleichs-Connector: `platform-core/modules/connectors/connectorTicketsJira.py` - Vergleichs-Method: `platform-core/modules/workflows/methods/methodJira/methodJira.py` - Vergleichs-Feature: `platform-core/modules/features/trustee/` (Config pro Instanz, Tools) - Bestehende Komponenten -- **wiederverwenden, nicht neubauen**: - `ui-nyla/src/components/PeriodPicker/` (Zeitraum-Auswahl mit Presets) - `ui-nyla/src/components/FormGenerator/FormGeneratorReport/` (Stats-Page-Renderer mit Charts via Recharts; akzeptiert PeriodPicker via `dateRangeSelector`) - `platform-core/modules/shared/dateRange.py` (`parseIsoDateRange`, `isoDateRangeToLocalEpoch`) - Stats-Vorbild im Code (Pattern für `serviceRedmineStats`): `platform-core/modules/workflows/methods/methodTrustee/actions/queryData.py` ## Kritischer Review (Pre-Execution) Dieser Abschnitt fasst Risiken, blinde Flecken und ungeklärte Punkte zusammen, die **vor** Phase 1 entweder akzeptiert, mitigiert oder eskaliert werden müssen. ### A. Stark / wahrscheinlich kein Problem 1. **HTML-Pilot deckt UI-Konzept ab**: 3 Seiten visuell validiert, User-Feedback eingebaut (Userstory-Wurzel, Orphans, Stats-First-Page). Risiko UI-Rework in Phase 2 ist minimal. 2. **Patterns vorhanden**: `connectorTicketsJira.py`, `methodJira`, `feature/trustee` (Config pro Instanz), `FormGeneratorReport`, `PeriodPicker`, `dateRange.py` -- keine Greenfield-Entscheidung mehr nötig, nur Anwenden. 3. **Pilot-Code für REST-Calls bewährt** (`_redmineClient.py`): Endpoint-Liste, Pagination, idempotente Update-Logik in produktivem Sync seit Wochen stabil. 4. **AI-Tool-Pattern** (Toolbox + ActionToolAdapter) ist in CommCoach/Trustee/Jira mehrfach umgesetzt -- Phase 3 ist Copy-Adapt, kein Pionierwerk. ### B. Mittlere Risiken / Frühzeitig prüfen 1. **Schema-Cache-TTL** (default 24h): Custom-Field-Änderungen in Redmine landen erst nach Refresh im UI. **Mitigation**: "Schema neu laden"-Button auf Config-Seite (im Pilot bereits), zusätzlich Auto-Refresh bei `400 unknown_custom_field`-Antwort. 2. **Throughput-Aggregation auf grossen Projekten**: 5'000+ Tickets würden bei `bucket=day` × 12 Monate = 365 Buckets × 5 Charts den Browser belasten. **Mitigation**: Backend liefert nur die aggregierten Sektions-Daten (nicht Roh-Tickets); zusätzlich `bucket`-Auto-Downgrade (Backend wechselt auf `week`/`month`, wenn >180 Buckets). 3. **PeriodPicker-Round-trip**: `FormGeneratorReport` synthetisiert `dateRange` aus `periodValue`. Bei Browser-Reload muss der Preset (z. B. `last12Months`) erhalten bleiben, sonst rebrechnet sich `lastN` jedes Mal anders. Bereits in Komponente gelöst (siehe `FormGeneratorReportTypes.ts`-Doku), aber **AC11/T11 explizit testen**. 4. **Orphan-Detection vs. Tracker-Filter**: Wenn der User im Browser z. B. nur "Bug" filtert, dann kann ein zur Userstory verknüpftes Bug fälschlich als "Orphan" wirken (weil die US wegen Filter unsichtbar ist). **Entscheid**: Orphan-Berechnung läuft IMMER über alle Tracker (US-Wurzeln werden für die Reachability nie ausgefiltert); nur die Anzeige folgt dem Filter. Diese Regel ist im Pilot bereits so umgesetzt -- in `serviceRedmineStats.orphanIdsFor()` als Kommentar dokumentieren. 5. **AI-Tool `getRedmineStats` vs. UI-Endpoint**: Beide nutzen `serviceRedmineStats` -- aber Tool-Output muss kompakter sein (Token-Budget). **Lösung**: Tool gibt nur `kpis` + Top-3 pro Section zurück, mit Hinweis "vollständige Daten unter UI-Statistik-Page". ### C. Entschieden (User 2026-04-21) 1. **DTO-Format**: ✓ **Roh-Buckets** im `RedmineStatsDto`, Mapping auf `ReportSection[]` im Frontend (`RedmineStatsPage.tsx`). Sauberer Layer, Backend bleibt unabhängig vom Frontend-Render-Schema. 2. **Multi-Instanz-Routing**: ✓ **Standard-Pattern wie Trustee** (Pfad-Parameter `/api/redmine/{instanceId}/...`, Multi-Instance pro Mandat, Instance-Resolution analog `featureInstanceContext`). Kein Sonderweg. 3. **Default-Periode pro Instanz**: ✓ **`default_period_value`-Spalte als optionaler `PeriodValue`-Snapshot**. Wird nur beim Erst-Öffnen verwendet; URL-Params `?dateFrom=&dateTo=` überschreiben. 4. **Connector-Tests**: ✓ **Direkt gegen den SSS-Case** (`redmine.logobject.ch`, Projekt myABI/SSS) -- keine Mock-HTTP-Server, keine VCR-Cassetten. Tests laufen als Live-Integration; bei CI-Lauf ohne Netzzugriff sauber `pytest.mark.live` skippen lassen. 5. **Stats-Caching**: ✓ **Sauberes Caching von Anfang an** -- TTL-basierter In-Memory-Cache pro `(instanceId, dateFrom, dateTo, bucket, trackerIds)`-Key, default 60s, mit Invalidierung auf `update/create/addRelation` für die betroffene Instance. ### D. Schwächen / Lücken im aktuellen Plan 1. **Keine Migration-Strategie für bestehende `default_period_value`-Werte**: Wenn wir später Presets ändern, brechen wir gespeicherte Snapshots. **Mitigation**: `PeriodValue`-Snapshot enthält fromDate/toDate als Fallback -- alte Presets werden im schlechtesten Fall als `custom` re-rehydriert, kein Datenverlust. 2. **Kein Hinweis im Plan auf Error-States im Stats-Chart**: Wenn `getStats` 500 wirft, was zeigt die Page? **Ergänzung in Phase 2**: `` und Error-Toast. 3. **Keine Bulk-Operation auf Stats-KPIs** (z. B. "alle Orphans als 'archived' markieren"): bewusst V2. 4. **Webhook-Subscription ist bewusst NICHT V1** -- d. h. Ticket-Änderungen in Redmine werden erst beim nächsten User-Reload sichtbar. Im Plan unter "Nicht-Ziele" festgehalten -- für Stakeholder transparent. 5. **i18n der Stats-Sektions-Titel**: User-facing strings ("Top Bearbeiter", "Throughput", ...) in Page-Komponente, nicht im Backend (Backend liefert nur Daten). Bereits Konvention -- explizit im Phase-2-Checklist erwähnen. ### E. Reihenfolge-Empfehlung der Ausführung 1. Phase 1 startet mit **`datamodelRedmine.py` + `connectorTicketsRedmine.py` + Connector-Tests** (T1) -- damit alle Folge-Module typisiert arbeiten. 2. Erst **dann** DB-Migration + `interfaceFeatureRedmine` + `serviceRedmine` (read-only). 3. **Davor schon**, parallel zu (1)-(2): `serviceRedmineStats` + Tests (T5) -- läuft unit-isoliert auf Mock-Tickets. 4. Erst wenn Backend GET-Pfade stehen, beginnen Frontend-Pages -- Mock-Mode in `redmineApi.ts` als Übergang nicht nötig (Pilot deckt das ab). 5. Phase 3 (AI-Tools / Workflow) ganz am Schluss -- braucht stabile Service-Layer. ### F. Definition of "Ready to Execute" - [x] HTML-Pilot vom User abgenommen - [x] Plan v2 (mit PeriodPicker, Stats-Page, Orphans, Decisions, AC) geschrieben - [x] Bestehende Komponenten/Patterns identifiziert - [x] Offene Fragen C1--C5 entschieden (User-Antworten 2026-04-21) - [x] Sandbox-Redmine bestätigt: `redmine.logobject.ch`, Projekt myABI/SSS -- Tests laufen direkt dort → ✅ **Plan ist executionsbereit.** Phase 1 kann starten. ### G. Phase-1 Start-Sequenz (operative Reihenfolge) 1. `platform-core/modules/features/redmine/datamodelRedmine.py` -- Pydantic-Modelle, **inkl.** `RedmineStatsDto` mit Roh-Bucket-Substrukturen 2. `platform-core/modules/connectors/connectorTicketsRedmine.py` -- Read-Methoden zuerst (`whoAmI`, `getProjectMeta`, `getIssue`, `listIssues`, `listRelations`), Write-Methoden danach 3. T1 (Live-Connector-Tests) parallel mit (2) -- Tests treiben Connector-API-Form 4. Helper-Skript `platform-core/tests/fixtures/captureRedmineSnapshot.py` -- ruft `listIssues` einmalig live, schreibt JSON-Fixture für Service-/Stats-Unit-Tests 5. `serviceRedmine.py` (Idempotenz, Throttle) + `serviceRedmineStats.py` (Aggregationen) + `serviceRedmineStatsCache.py` 6. T3, T5, T12, T13 (Unit auf Fixture) 7. DB-Migration + `interfaceFeatureRedmine.py` (Config-Persistenz, Schema-Cache) 8. `mainRedmine.py` + `routeFeatureRedmine.py` + RBAC + Audit-Log 9. T2, T4, T8, T10 (API-Tests) 10. → Phase 2 (Frontend) startet erst, wenn Phase 1 grün ist. ## Abschluss - [ ] `wiki/b-reference/platform-core/features/redmine.md` aktualisiert (NEU anlegen) - [ ] `wiki/TOPICS.md` aktualisiert (Eintrag "Redmine Feature") - [ ] Dieses Dokument → `z-archive/` verschoben