From d6c9d641a737b0270a661172bca93387eee2b046 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Tue, 21 Apr 2026 18:14:17 +0200 Subject: [PATCH] redmine integration --- b-reference/gateway/ai-agent.md | 14 ++ c-work/1-plan/2026-04-redmine-feature.md | 290 +++++++++++++++++++++++ d-guides/coding-conventions.md | 17 ++ 3 files changed, 321 insertions(+) create mode 100644 c-work/1-plan/2026-04-redmine-feature.md diff --git a/b-reference/gateway/ai-agent.md b/b-reference/gateway/ai-agent.md index 2a56c28..04106e8 100644 --- a/b-reference/gateway/ai-agent.md +++ b/b-reference/gateway/ai-agent.md @@ -45,6 +45,20 @@ Pro Request propagiert der **ServiceCenterContext** u. a. `userId`, `mandateId`, | `toolSet` | Aktives Tool-Set (Default `"core"`) | | `temperature` | Optional für den Agent-AI-Call | +### System-Prompt: temporaler Kontext (Ist) + +`buildSystemPrompt` (`conversationManager.py`) injiziert beim Bauen des System-Prompts einen **Datums-/Zeitblock** (`_buildTemporalContext`). Der Sub-Agent-Prompt in `featureDataAgent._buildSchemaContext` macht dasselbe. Ohne diesen Block halluzinieren LLMs das aktuelle Datum aus ihrem Training-Cutoff (typisch: »Juni 2025«), was bei relativen Zeit-Filtern (»letzten Monat«, »Q1«) sofort zu falschen SQL-Filtern und falschen Antworten führt. + +Datenfluss der Zeitzone: + +1. **Browser**: `Intl.DateTimeFormat().resolvedOptions().timeZone` (z. B. `"Europe/Zurich"`). +2. **Frontend**: `frontend_nyla/src/api.ts` Axios-Interceptor sendet den IANA-Namen als `X-User-Timezone`-Header. +3. **Gateway**: `_requestContextMiddleware` (`app.py`) liest den Header und schreibt ihn via `_setRequestTimezone` in eine `ContextVar`. +4. **Konsumenten**: `getRequestNow()` / `getRequestTimezone()` aus `modules/shared/timeUtils.py` liefern die Werte für den Prompt-Block (gleiche Pattern wie `_setLanguage` / `_getLanguage`). +5. **Fallback**: Header fehlt oder ist ungültig → `UTC`. Niemals server-seitige Hardcoded-TZ. + +Storage bleibt UTC (`getUtcTimestamp`, `getIsoTimestamp`). Nur **user-sichtbare** Now-Werte (Agent-Prompt, formatierte Display-Strings) gehen über `getRequestNow()`. + ### Toolbox Registry **Datei:** `serviceCenter/services/serviceAgent/toolboxRegistry.py` diff --git a/c-work/1-plan/2026-04-redmine-feature.md b/c-work/1-plan/2026-04-redmine-feature.md new file mode 100644 index 0000000..9ff7388 --- /dev/null +++ b/c-work/1-plan/2026-04-redmine-feature.md @@ -0,0 +1,290 @@ + + + + +# 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 `gateway/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/gateway/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/gateway/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`) | 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: +- 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 via `dateRangeSelector`) + - `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 +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. `gateway/modules/features/redmine/datamodelRedmine.py` -- Pydantic-Modelle, **inkl.** `RedmineStatsDto` mit Roh-Bucket-Substrukturen +2. `gateway/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 `gateway/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/gateway/features/redmine.md` aktualisiert (NEU anlegen) +- [ ] `wiki/TOPICS.md` aktualisiert (Eintrag "Redmine Feature") +- [ ] Dieses Dokument → `z-archive/` verschoben diff --git a/d-guides/coding-conventions.md b/d-guides/coding-conventions.md index 6b59ac2..0f94565 100644 --- a/d-guides/coding-conventions.md +++ b/d-guides/coding-conventions.md @@ -143,6 +143,23 @@ zu machen. Referenz-Implementation: `modules/features/trustee/accounting/accountingDataSync.py` (jede Phase: `await connector.fetch(...)` -> `await asyncio.to_thread(self._persistXxx, ...)`). +### Datum/Zeit: UTC fuer Storage, Request-TZ fuer User-sichtbare Werte + +Backend-Code, der **gespeicherte** Zeitstempel produziert (DB-Felder, Audit-Logs, Token-Expiry), nutzt **UTC** via `getUtcTimestamp()`, `getUtcNow()` oder `getIsoTimestamp()` aus `modules/shared/timeUtils.py`. + +Backend-Code, der **user-sichtbare** "jetzt"-Werte produziert (AI-Agent-System-Prompt, gerenderte Display-Strings, formatierte Logs an den Endnutzer), nutzt `getRequestNow()` / `getRequestTimezone()`. Diese lesen die Browser-Zeitzone, die das Frontend per `X-User-Timezone`-Header schickt (Axios-Interceptor in `frontend_nyla/src/api.ts`) und die `_requestContextMiddleware` (`app.py`) in eine `ContextVar` schreibt — analog zum `_setLanguage`-Pattern. + +```python +from modules.shared.timeUtils import getUtcTimestamp, getRequestNow + +createdAt = getUtcTimestamp() # DB-Speicherung -> UTC float +nowForUser = getRequestNow() # Anzeige/Prompt -> tz-aware datetime in User-TZ +``` + +**Verboten:** Hardcoded-Zeitzonen wie `ZoneInfo("Europe/Zurich")` als Default fuer User-Anzeige. Stattdessen die Request-TZ benutzen; ohne HTTP-Kontext (z.B. Scheduler) faellt `getRequestNow()` automatisch auf UTC zurueck. + +**Frontend:** User-sichtbare Zeitstempel werden mit `formatUnixTimestamp()` aus `frontend_nyla/src/utils/time.ts` formatiert; ohne expliziten `timeZone`-Override nimmt `toLocaleString` die Browser-TZ. Backend-Felder bleiben UTC-Floats (`number`). + ### Debug-File-Dumps: nur DEV, niemals INT/PROD Code, der zu Debug-Zwecken raw payloads / Prompts / Sync-Daten auf die Disk