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