32 KiB
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
RedmineInstanceConfigstehtrootTrackerName(DefaultUserstory); beim Schema-Fetch wird dieser Name gegen die Tracker-Liste des Projekts case-insensitive aufgelöst. Falls der Tracker nicht existiert, istrootTrackerId=Noneund das UI zeigt einen Konfig-Fehler. - Lokaler Mirror für 20k+ Tickets. Tickets und Relations werden in
poweron_redminegespiegelt (RedmineTicketMirror,RedmineRelationMirror, beidePowerOnModelmit 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 ServiceserviceRedmineSyncmit per-Instanzasyncio.Lock, RoutesPOST /api/redmine/{instanceId}/sync?force=undGET /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 viadateFrom/dateToan das Backend (analoggateway/modules/shared/dateRange.py). Im Stats-Page wirdPeriodPickerdirekt vomFormGeneratorReport.dateRangeSelectorgerendert; 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/POSTaktuellen Stand lesen, nur Diff schreiben (sieheapplyPlan.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_fieldsbeim Connect und cachen das Schema pro Instanz; Edit-Form rendert dynamisch (analogFormGenerator). - Workflow-Limits.
DELETE /issuesist mit unseren Keys typischerweise verboten (HTTP 403);Closed+resolution_type=invalidals Soft-Delete. Statuswechsel ausserhalb des erlaubten Workflows liefern204ohne Effekt -- nach jedemPUTvalidieren. - 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 durcht()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
redminemit 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
methodRedminemit 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, analogconnectorTicketsJira.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 inregisterCore.py(Toolboxredmine, 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) -- ersetztRedmineTopologyPage; rendert primär<FormGeneratorReport>mit aktiviertemdateRangeSelector(= PeriodPicker),filters(Tracker-Multiselect) undperiodSelector(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,testConfigconfig/pageRegistry.tsx-- 3 Seiten registrieren (Default-Page = Stats)- PeriodPicker:
components/PeriodPicker-- ohne Änderung wiederverwendet (in Stats indirekt viaFormGeneratorReport.dateRangeSelector, im Browser direkt).
- DB-Migration:
- Ja, eine Tabelle
RedmineInstanceConfig(FK aufFeatureInstance) -- speichert Base-URL, projectId, encrypted apiKey, Default-Filter (incl.defaultPeriodPresetals JSON-Snapshot desPeriodValue),rootTrackerId(default Userstory), Schema-Cache-TTL. Schema-Daten (Trackers, Statuses, CFs) liegen in einer separaten JSON-Spalte als Cache.
- Ja, eine Tabelle
- Andere:
wiki/b-reference/gateway/features/redmine.md(NEU, beim Done)wiki/TOPICS.mdEintrag (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
- Mock-JSON aus
tickets-redmine.csvextrahieren (~19 Tickets, alle Tracker + Beziehungstypen + 4 Orphans) local/notes/redmine-pilot/index.html-- Tabs: Statistik (Default), Browser, Config- Statistik-View: KPI-Tiles + 5 SVG-Charts (Status-pro-Tracker, Throughput, Top-Bearbeiter, Beziehungs-Donut, Backlog-Aging) als Vorlage für
FormGeneratorReport - Browser-View: Tree links (Userstory-Wurzeln + virtueller Orphan-Wurzelknoten, kollabierbar, Multi-Filter), rechts Edit-Form
- Config-View: Form für Base-URL, API-Key, Project-ID, Default-Filter, Wurzel-Tracker
- 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_configTabelle, FK auffeature_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); akzeptiertdateFrom/dateTo(viadateRange.parseIsoDateRange) +bucket(day|week|month) + optionaltrackerIds. Berechnet Orphan-Count = Tickets nicht erreichbar von einer User-Story-Wurzel via Relationen jeden Typs. Liefert Roh-Buckets imRedmineStatsDto(keinFormGeneratorReport-Schema).serviceRedmineStatsCache.py-- TTL-Cache (default 60s) keyed auf(instanceId, dateFrom, dateTo, bucket, sortedTrackerIds). Invalidierung pro Instance beiupdate/create/addRelation-Calls inserviceRedmine.mainRedmine.py--FEATURE_CODE="redmine",UI_OBJECTS=["redmineStats", "redmineBrowser", "redmineConfig"],DATA_OBJECTS,TEMPLATE_ROLESrouteFeatureRedmine.py--/api/redmine/{instanceId}/...:GET stats?dateFrom=&dateTo=&bucket=&trackers=→RedmineStatsDtoGET tickets?dateFrom=&dateTo=&trackers=&status=&search=→list[RedmineTicketDto]GET tickets/{id},PUT tickets/{id},POST tickets,POST tickets/{id}/relationsGET 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 indefault_period_valuegespeichert)RedmineStatsPage-- nutzt<FormGeneratorReport>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<PeriodPicker>(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 ausmeta.customFields(FormGenerator), Status-Dropdown, Subject, Description, Assignee, Tracker, Beziehungs-Liste mit Klick-NavigationredmineApi.ts--useApi-Wrapper für alle Endpoints;getStats({dateFrom, dateTo, bucket, trackers})mit Debounce 300mspageRegistry.tsx-- 3 Seiten lazy-registriert mitobjectKey, 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 Toolboxredmine(immer aktiv bei vorhandener Feature-Instanz):readRedmineTicket(issueId, includeRelations)-- readOnlysearchRedmineTickets(query, tracker, status, dateFrom, dateTo)-- readOnlylistRedmineMeta()-- readOnly (Trackers, Statuses, CFs)getRedmineStats(dateFrom, dateTo, bucket, trackers)-- readOnly, gleiche Aggregation wie UIupdateRedmineTicket(issueId, fields, notes)-- writecreateRedmineTicket(subject, description, tracker, customFields)-- writeaddRedmineRelation(fromId, toId, relationType)-- write
toolboxRegistry.py-- Toolbox-DefinitionredminemitfeatureCode="redmine"methodRedmine+ Actions analogmethodJira--actionId="redmine.<action>",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/gateway/features/redmine.mdneu anlegen (mit Sektion "Statistik-Aggregationen")wiki/TOPICS.mdEintrag- 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: <In Progress>} 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) |
gateway/tests/test_connector_redmine_live.py (whoAmI, getProjectMeta, getIssue, listIssues paginated) | pending |
| T2 | 1 | api | ja | gateway/tests/test_route_redmine_config.py | pending |
| T3 | 2 | unit | ja | gateway/tests/test_service_redmine_orphans.py (Tree-Builder + Orphan-Detection auf Fixture-JSON) | pending |
| T4 | 3 | live-integration | ja (mark.live) |
gateway/tests/test_route_redmine_update_live.py (Read-modify-write Round-trip) | pending |
| T5 | 4,11 | unit | ja | gateway/tests/test_service_redmine_stats.py (Period-Filter, Tracker-Filter, Bucket-Aggregation auf Fixture-JSON) | pending |
| T6 | 5,6,12 | integration | ja | gateway/tests/test_redmine_tools.py (mark.live für Write-Tools) |
pending |
| T7 | 7 | integration | ja | gateway/tests/test_method_redmine.py | pending |
| T8 | 8 | api | ja | gateway/tests/test_route_redmine_delete.py (Soft-Delete-Mapping + Toast-Message) | pending |
| T9 | 9 | ui | manuell | -- | pending |
| T10 | 10 | api | ja | gateway/tests/test_route_redmine_rbac.py | pending |
| T11 | 4 | ui | manuell | -- (PeriodPicker-Round-trip in Stats) | pending |
| T12 | 13 | unit | ja | gateway/tests/test_service_redmine_stats.py::test_unassigned_bucket | pending |
| T13 | -- | unit | ja | gateway/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: https://www.redmine.org/projects/redmine/wiki/Rest_api
- Vergleichs-Connector:
gateway/modules/connectors/connectorTicketsJira.py - Vergleichs-Method:
gateway/modules/workflows/methods/methodJira/methodJira.py - Vergleichs-Feature:
gateway/modules/features/trustee/(Config pro Instanz, Tools) - Bestehende Komponenten -- wiederverwenden, nicht neubauen:
frontend_nyla/src/components/PeriodPicker/(Zeitraum-Auswahl mit Presets)frontend_nyla/src/components/FormGenerator/FormGeneratorReport/(Stats-Page-Renderer mit Charts via Recharts; akzeptiert PeriodPicker viadateRangeSelector)gateway/modules/shared/dateRange.py(parseIsoDateRange,isoDateRangeToLocalEpoch)
- Stats-Vorbild im Code (Pattern für
serviceRedmineStats):gateway/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
- 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.
- Patterns vorhanden:
connectorTicketsJira.py,methodJira,feature/trustee(Config pro Instanz),FormGeneratorReport,PeriodPicker,dateRange.py-- keine Greenfield-Entscheidung mehr nötig, nur Anwenden. - Pilot-Code für REST-Calls bewährt (
_redmineClient.py): Endpoint-Liste, Pagination, idempotente Update-Logik in produktivem Sync seit Wochen stabil. - 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
- 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. - 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ätzlichbucket-Auto-Downgrade (Backend wechselt aufweek/month, wenn >180 Buckets). - PeriodPicker-Round-trip:
FormGeneratorReportsynthetisiertdateRangeausperiodValue. Bei Browser-Reload muss der Preset (z. B.last12Months) erhalten bleiben, sonst rebrechnet sichlastNjedes Mal anders. Bereits in Komponente gelöst (sieheFormGeneratorReportTypes.ts-Doku), aber AC11/T11 explizit testen. - 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. - AI-Tool
getRedmineStatsvs. UI-Endpoint: Beide nutzenserviceRedmineStats-- aber Tool-Output muss kompakter sein (Token-Budget). Lösung: Tool gibt nurkpis+ Top-3 pro Section zurück, mit Hinweis "vollständige Daten unter UI-Statistik-Page".
C. Entschieden (User 2026-04-21)
- DTO-Format: ✓ Roh-Buckets im
RedmineStatsDto, Mapping aufReportSection[]im Frontend (RedmineStatsPage.tsx). Sauberer Layer, Backend bleibt unabhängig vom Frontend-Render-Schema. - Multi-Instanz-Routing: ✓ Standard-Pattern wie Trustee (Pfad-Parameter
/api/redmine/{instanceId}/..., Multi-Instance pro Mandat, Instance-Resolution analogfeatureInstanceContext). Kein Sonderweg. - Default-Periode pro Instanz: ✓
default_period_value-Spalte als optionalerPeriodValue-Snapshot. Wird nur beim Erst-Öffnen verwendet; URL-Params?dateFrom=&dateTo=überschreiben. - 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 sauberpytest.mark.liveskippen lassen. - Stats-Caching: ✓ Sauberes Caching von Anfang an -- TTL-basierter In-Memory-Cache pro
(instanceId, dateFrom, dateTo, bucket, trackerIds)-Key, default 60s, mit Invalidierung aufupdate/create/addRelationfür die betroffene Instance.
D. Schwächen / Lücken im aktuellen Plan
- 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 alscustomre-rehydriert, kein Datenverlust. - Kein Hinweis im Plan auf Error-States im Stats-Chart: Wenn
getStats500 wirft, was zeigt die Page? Ergänzung in Phase 2:<FormGeneratorReport noDataMessage>und Error-Toast. - Keine Bulk-Operation auf Stats-KPIs (z. B. "alle Orphans als 'archived' markieren"): bewusst V2.
- 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.
- 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
- Phase 1 startet mit
datamodelRedmine.py+connectorTicketsRedmine.py+ Connector-Tests (T1) -- damit alle Folge-Module typisiert arbeiten. - Erst dann DB-Migration +
interfaceFeatureRedmine+serviceRedmine(read-only). - Davor schon, parallel zu (1)-(2):
serviceRedmineStats+ Tests (T5) -- läuft unit-isoliert auf Mock-Tickets. - Erst wenn Backend GET-Pfade stehen, beginnen Frontend-Pages -- Mock-Mode in
redmineApi.tsals Übergang nicht nötig (Pilot deckt das ab). - Phase 3 (AI-Tools / Workflow) ganz am Schluss -- braucht stabile Service-Layer.
F. Definition of "Ready to Execute"
- HTML-Pilot vom User abgenommen
- Plan v2 (mit PeriodPicker, Stats-Page, Orphans, Decisions, AC) geschrieben
- Bestehende Komponenten/Patterns identifiziert
- Offene Fragen C1--C5 entschieden (User-Antworten 2026-04-21)
- 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)
gateway/modules/features/redmine/datamodelRedmine.py-- Pydantic-Modelle, inkl.RedmineStatsDtomit Roh-Bucket-Substrukturengateway/modules/connectors/connectorTicketsRedmine.py-- Read-Methoden zuerst (whoAmI,getProjectMeta,getIssue,listIssues,listRelations), Write-Methoden danach- T1 (Live-Connector-Tests) parallel mit (2) -- Tests treiben Connector-API-Form
- Helper-Skript
gateway/tests/fixtures/captureRedmineSnapshot.py-- ruftlistIssueseinmalig live, schreibt JSON-Fixture für Service-/Stats-Unit-Tests serviceRedmine.py(Idempotenz, Throttle) +serviceRedmineStats.py(Aggregationen) +serviceRedmineStatsCache.py- T3, T5, T12, T13 (Unit auf Fixture)
- DB-Migration +
interfaceFeatureRedmine.py(Config-Persistenz, Schema-Cache) mainRedmine.py+routeFeatureRedmine.py+ RBAC + Audit-Log- T2, T4, T8, T10 (API-Tests)
- → Phase 2 (Frontend) startet erst, wenn Phase 1 grün ist.
Abschluss
wiki/b-reference/gateway/features/redmine.mdaktualisiert (NEU anlegen)wiki/TOPICS.mdaktualisiert (Eintrag "Redmine Feature")- Dieses Dokument →
z-archive/verschoben