From b85a6ec0c7af8deb1fc2b54266b8d5399dbed11c Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 11 Jul 2026 09:48:14 +0200 Subject: [PATCH] AP1.6b: Journey um Gate-Fortschritt und Ist-Graph erweitern. Fokus-Gates mit Kriterien-Checkliste und eingebetteter Graph-Sicht in Kontrolle; Abnahme-Reihenfolge in Roadmap dokumentiert. Co-authored-by: Cursor --- .../Kairo_Corrected_MVP_Roadmap_v0.2.md | 18 ++- .../Kairo_Implementation_Truth_Table_v0.1.md | 5 +- frontend/src/components/GateMapView.jsx | 27 ++-- .../src/components/JourneyGateMapSection.jsx | 25 +++ .../components/JourneyGateProgressSection.jsx | 142 ++++++++++++++++++ frontend/src/components/JourneyTimeline.jsx | 16 +- .../initiative/InitiativeJourneyPage.jsx | 73 +++++++++ frontend/src/styles/components.css | 51 +++++++ 8 files changed, 331 insertions(+), 26 deletions(-) create mode 100644 frontend/src/components/JourneyGateMapSection.jsx create mode 100644 frontend/src/components/JourneyGateProgressSection.jsx diff --git a/docs/product/Kairo_Corrected_MVP_Roadmap_v0.2.md b/docs/product/Kairo_Corrected_MVP_Roadmap_v0.2.md index db7a1fc..89e6bad 100644 --- a/docs/product/Kairo_Corrected_MVP_Roadmap_v0.2.md +++ b/docs/product/Kairo_Corrected_MVP_Roadmap_v0.2.md @@ -142,6 +142,17 @@ Phase L Validation AP0.10d (laufend PO) parallel ## 4. Nächste Pakete (verbindliche Reihenfolge — PO 2026-07-06) +### 4.0 Abnahme-Reihenfolge (Stand 2026-07-11) + +| # | Paket | Status | Begründung | +|---|-------|--------|------------| +| 1 | **AP1.6b** Journey Gate-Fortschritt + Ist-Graph | ✓ | ADP-Lücke schließen; Plan/Ist in Kontrolle sichtbar | +| 2 | **AP1.9c/d** Cockpit + Kontrolle Polish | offen | Signale auf Portfolio-Kacheln, Filter | +| 3 | **AP1.5d** Rekursive Tasks (Rest) | → | Backend + Outline da; ggf. Task-Detail-Route | +| 4 | **AP1.8b** Situativer Kontext | deferred | Nach Portfolio-Prio (8a ✓) | +| 5 | **AP1.7** Agent Interface | offen | Nach Operating Model tragfähig | +| 6 | **AP1.15d** OR-Alternativpfade | deferred | Nur bei Use Case | + ### AP1.5 — Hierarchie & Ist-Ebene ✓ **Ziel:** Operativer Plan zwischen Gates — Project, Task, Ausführung-Tab; Gates optional nutzbar. @@ -193,7 +204,12 @@ Backlog/Action/Project/Task → RoadmapItem; Decision-Spur; Journey-Timeline. **Nach AP1.5c** — Gates bekommen Bedeutung durch Ist-Bezug (auch auf Project-Ebenen). -**Version:** `0.15.0-ap1.6` +| Slice | Scope | Status | +|-------|--------|--------| +| **6a** | Plan/Ist-Links, Journey-Timeline, Gate-Beiträge | ✓ | +| **6b** | Journey: Fokus-Gates + Kriterien (read-only), eingebetteter Ist-Graph | ✓ | + +**Version:** `0.15.0-ap1.6` / `0.15.0-ap1.6b` --- diff --git a/docs/product/Kairo_Implementation_Truth_Table_v0.1.md b/docs/product/Kairo_Implementation_Truth_Table_v0.1.md index a3db155..d938a25 100644 --- a/docs/product/Kairo_Implementation_Truth_Table_v0.1.md +++ b/docs/product/Kairo_Implementation_Truth_Table_v0.1.md @@ -1,7 +1,7 @@ # Kairo — Implementation Truth Table v0.1 **Status:** living document — bei jedem AP aktualisieren -**Stand:** 2026-07-10 (nach AP1.13) +**Stand:** 2026-07-11 (nach AP1.6b) **Zweck:** Ehrliche Trennung von **implementiert**, **teilweise**, **nur API/Schema**, **nur Dokumentiert** Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden. @@ -87,6 +87,7 @@ Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden. | Workspace (Portfolio aller Initiativen) | ✓ | Portfolio-Widget | | Initiative-Übersicht (operative Steuerung) | ✓ | AP1.2c | | Initiative Unterseiten (Plan, Inbox, …) | ✓ | Shell + Pflege verschoben | +| Journey (Kontrolle / Initiative) | ◐ | AP1.6b: Fokus-Gates + Kriterien read-only, Ist-Graph eingebettet | | Next-Action-Widget konfigurierbar (Workspace + Initiative) | ✓ | scope portfolio/initiative | | Action Detail-Route | ✓ | `/initiatives/:id/actions/:actionId` | | Action Detail-Seite | ✗ | | @@ -104,7 +105,7 @@ Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden. | Zieltermin | ◐ | `target_date` | | Abhängigkeiten | ◐ | API; Graph Engine AP1.4d | | parallel vs. sequenziell | ◐ | `sequencing_mode` (deprecated); Graph AP1.4d | -| Plan-Graph vs. Ist-Overlay | ✗ | AP1.13: ein Graph + Status am Knoten — Plan/Ist vermischt | +| Plan-Graph vs. Ist-Overlay | ◐ | AP1.13 Designer + AP1.6b Journey Ist-Graph (read-only); Plan/Ist-Diff AP1.14 | | Zielzustands-Designer (Joint/Parallel) | ◐ | AP1.15a/b Designer; AP1.15c parallel/optional in Engine+Designer; OR deferred | | Plan-Revision / Graph-Historie | ◐ | AP1.14 Plan-Snapshots | | Verify vor `reached` | ◐ | Kriterienplan + gate_override | diff --git a/frontend/src/components/GateMapView.jsx b/frontend/src/components/GateMapView.jsx index 52f9138..db164e7 100644 --- a/frontend/src/components/GateMapView.jsx +++ b/frontend/src/components/GateMapView.jsx @@ -18,7 +18,7 @@ function truncateTitle(title, max = 28) { return `${title.slice(0, max - 1)}…` } -export function GateMapView({ initiativeId, items, graphState: graphStateProp }) { +export function GateMapView({ initiativeId, items, graphState: graphStateProp, embedded = false }) { const navigate = useNavigate() const [dependencies, setDependencies] = useState([]) const [graphStateLocal, setGraphStateLocal] = useState(null) @@ -80,17 +80,22 @@ export function GateMapView({ initiativeId, items, graphState: graphStateProp }) return } + const Wrapper = embedded ? 'div' : 'section' + const wrapperClass = embedded ? 'gate-map-view gate-map-view--embedded' : 'card gate-map-view' + return ( -
-
-
-

Zielzustände (Graph)

-

- Read-only Übersicht der Gate-Abhängigkeiten — im Designer modellieren oder auf der - Gate-Detailseite Kanten pflegen; Verify und Kriterien dort. -

+ + {!embedded && ( +
+
+

Zielzustände (Graph)

+

+ Read-only Übersicht der Gate-Abhängigkeiten — im Designer modellieren oder auf der + Gate-Detailseite Kanten pflegen; Verify und Kriterien dort. +

+
-
+ )}
@@ -209,6 +214,6 @@ export function GateMapView({ initiativeId, items, graphState: graphStateProp }) „Abhängigkeiten“ anlegen; ohne Kanten gelten Gates nach Reihenfolge.

)} -
+ ) } diff --git a/frontend/src/components/JourneyGateMapSection.jsx b/frontend/src/components/JourneyGateMapSection.jsx new file mode 100644 index 0000000..b34cc87 --- /dev/null +++ b/frontend/src/components/JourneyGateMapSection.jsx @@ -0,0 +1,25 @@ +import { GateMapView } from './GateMapView.jsx' + +export function JourneyGateMapSection({ initiativeId, items, graphState, loading }) { + if (!items.length) return null + + return ( +
+

Zielzustands-Graph (Ist)

+

+ Baum der Gate-Abhängigkeiten mit Ist-Status — keine Planungsbearbeitung (Kontrolle / + Journey). +

+ {loading ? ( +

Graph wird geladen …

+ ) : ( + + )} +
+ ) +} diff --git a/frontend/src/components/JourneyGateProgressSection.jsx b/frontend/src/components/JourneyGateProgressSection.jsx new file mode 100644 index 0000000..5e192c7 --- /dev/null +++ b/frontend/src/components/JourneyGateProgressSection.jsx @@ -0,0 +1,142 @@ +import { Link } from 'react-router-dom' +import { gateTitleById } from './GateSelect.jsx' +import { gatePath } from '../utils/routes.js' +import { CRITERION_STATUS_LABELS } from '../constants/status.js' +import { StatusBadge } from './StatusBadge.jsx' + +export function GateSteeringHint({ graphState, itemId, siblingItems = [] }) { + if (!graphState?.items?.[itemId]) return null + + const itemState = graphState.items[itemId] + const enforceBlocking = graphState.graph_profile?.enforce_gate_blocking !== false + const blockedBy = itemState.blocked_by || [] + const wouldBlock = itemState.would_block ?? itemState.blocked + + if (itemState.ready) { + return Bereit + } + + if (wouldBlock && blockedBy.length > 0) { + const blockerTitle = + blockedBy.length === 1 ? gateTitleById(siblingItems, blockedBy[0]) : null + const title = + blockerTitle != null + ? `Wartet auf: ${blockerTitle}` + : `Wartet auf ${blockedBy.length} Voraussetzungen` + const className = enforceBlocking + ? 'status-pill status-pill--blocked' + : 'status-pill status-pill--wait' + const label = enforceBlocking ? 'Blockiert' : 'Wartet' + return ( + + {label} + + ) + } + + return null +} + +function sortGatesForJourney(items, graphState) { + const terminal = new Set(['reached', 'moved', 'discarded']) + const open = items.filter((item) => !terminal.has(item.status)) + return [...open].sort((a, b) => { + const aState = graphState?.items?.[a.id] + const bState = graphState?.items?.[b.id] + const aReady = aState?.ready ? 0 : 1 + const bReady = bState?.ready ? 0 : 1 + if (aReady !== bReady) return aReady - bReady + const aWait = aState?.would_block || aState?.blocked ? 1 : 0 + const bWait = bState?.would_block || bState?.blocked ? 1 : 0 + if (aWait !== bWait) return aWait - bWait + return (a.sort_order ?? 0) - (b.sort_order ?? 0) + }) +} + +export { sortGatesForJourney } + +export function JourneyGateProgressSection({ + items = [], + graphState, + criteriaProgress = {}, + criteriaByGate = {}, + loading = false, +}) { + if (loading) { + return ( +
+

Zielzustands-Fortschritt wird geladen …

+
+ ) + } + + if (!items.length) { + return null + } + + const focusGates = sortGatesForJourney(items, graphState).slice(0, 5) + + return ( +
+

Zielzustands-Fortschritt

+

+ Nächste Gates und Kriterien im Ist — Schritt für Schritt bis Verify. Vollständige Pflege + auf der Gate-Detailseite. +

+
    + {focusGates.map((gate) => { + const progress = criteriaProgress[gate.id] + const criteria = criteriaByGate[gate.id] || [] + const openCriteria = criteria.filter( + (c) => !['satisfied', 'waived', 'deferred'].includes(c.status), + ) + return ( +
  • +
    +
    + + {gate.title} + + + +
    + {progress?.total > 0 && ( +

    + Kriterien: {progress.closed}/{progress.total} erfüllt +

    + )} + {criteria.length > 0 && ( +
      + {criteria.map((criterion) => ( +
    1. + {criterion.title} + + {CRITERION_STATUS_LABELS[criterion.status] || criterion.status} + +
    2. + ))} +
    + )} + {openCriteria.length === 0 && criteria.length > 0 && gate.status !== 'reached' && ( +

    Alle Kriterien bearbeitet — Verify prüfen.

    + )} +
    +
  • + ) + })} +
+
+ ) +} diff --git a/frontend/src/components/JourneyTimeline.jsx b/frontend/src/components/JourneyTimeline.jsx index 2835f83..717b9d6 100644 --- a/frontend/src/components/JourneyTimeline.jsx +++ b/frontend/src/components/JourneyTimeline.jsx @@ -1,4 +1,5 @@ import { Link } from 'react-router-dom' +import { actionPath, gatePath } from '../utils/routes.js' const KIND_LABELS = { decision: 'Entscheidung', @@ -40,26 +41,17 @@ export function JourneyTimeline({ events, initiativeId, roadmapItems = [] }) { {event.summary &&

{event.summary}

}
{event.roadmap_item_id && gateTitles[event.roadmap_item_id] && ( - + Gate: {gateTitles[event.roadmap_item_id]} )} {event.kind === 'action' && ( - + Arbeitspaket öffnen )} {event.kind === 'gate' && ( - + Gate öffnen )} diff --git a/frontend/src/pages/initiative/InitiativeJourneyPage.jsx b/frontend/src/pages/initiative/InitiativeJourneyPage.jsx index b2dc25a..a5abbf8 100644 --- a/frontend/src/pages/initiative/InitiativeJourneyPage.jsx +++ b/frontend/src/pages/initiative/InitiativeJourneyPage.jsx @@ -1,11 +1,18 @@ import { useCallback, useEffect, useState } from 'react' import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx' import { listInitiativeJourney } from '../../api/journey.js' +import { + listInitiativeRoadmapCriteriaProgress, + listInitiativeRoadmapGraphState, + listRoadmapItemCriteria, +} from '../../api/roadmap.js' import { DecisionsSection } from '../../components/DecisionsSection.jsx' import { ReviewsSection } from '../../components/ReviewsSection.jsx' import { RecurringSection } from '../../components/RecurringSection.jsx' import { EvidenceSection } from '../../components/EvidenceSection.jsx' import { JourneyTimeline } from '../../components/JourneyTimeline.jsx' +import { JourneyGateProgressSection, sortGatesForJourney } from '../../components/JourneyGateProgressSection.jsx' +import { JourneyGateMapSection } from '../../components/JourneyGateMapSection.jsx' export function InitiativeJourneyPage() { const ops = useInitiativeOperations() @@ -13,6 +20,10 @@ export function InitiativeJourneyPage() { const [journeyEvents, setJourneyEvents] = useState([]) const [journeyLoading, setJourneyLoading] = useState(true) const [journeyError, setJourneyError] = useState(null) + const [graphState, setGraphState] = useState(null) + const [criteriaProgress, setCriteriaProgress] = useState({}) + const [criteriaByGate, setCriteriaByGate] = useState({}) + const [gateMetaLoading, setGateMetaLoading] = useState(true) const loadJourney = useCallback(async () => { if (!id) return @@ -28,6 +39,53 @@ export function InitiativeJourneyPage() { } }, [id]) + useEffect(() => { + if (!id || !roadmapItems.length) { + setGraphState(null) + setCriteriaProgress({}) + setCriteriaByGate({}) + setGateMetaLoading(false) + return undefined + } + let cancelled = false + setGateMetaLoading(true) + + Promise.all([ + listInitiativeRoadmapGraphState(id), + listInitiativeRoadmapCriteriaProgress(id), + ]) + .then(async ([graph, progress]) => { + if (cancelled) return + setGraphState(graph || null) + setCriteriaProgress(progress || {}) + const sortedFocus = sortGatesForJourney(roadmapItems, graph || null) + .slice(0, 5) + .map((item) => item.id) + const criteriaEntries = await Promise.all( + sortedFocus.map((gateId) => + listRoadmapItemCriteria(gateId) + .then((criteria) => [gateId, Array.isArray(criteria) ? criteria : []]) + .catch(() => [gateId, []]), + ), + ) + if (cancelled) return + setCriteriaByGate(Object.fromEntries(criteriaEntries)) + }) + .catch(() => { + if (!cancelled) { + setGraphState(null) + setCriteriaProgress({}) + setCriteriaByGate({}) + } + }) + .finally(() => { + if (!cancelled) setGateMetaLoading(false) + }) + return () => { + cancelled = true + } + }, [id, roadmapItems]) + useEffect(() => { loadJourney() }, [loadJourney, ops.decisions, ops.reviews, ops.evidenceItems, ops.actions]) @@ -40,6 +98,21 @@ export function InitiativeJourneyPage() { <> {error &&

{error}

} + + + +

Journey

diff --git a/frontend/src/styles/components.css b/frontend/src/styles/components.css index 3809304..aac3df3 100644 --- a/frontend/src/styles/components.css +++ b/frontend/src/styles/components.css @@ -1255,6 +1255,52 @@ box-shadow: none; } +.journey-gate-progress-list { + margin-top: 0.75rem; +} + +.journey-gate-progress-item__head { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem 0.75rem; +} + +.journey-gate-criteria-list { + margin: 0.5rem 0 0; + padding-left: 1.25rem; + display: grid; + gap: 0.35rem; +} + +.journey-gate-criterion { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.35rem 0.75rem; +} + +.journey-gate-criterion--done { + opacity: 0.72; +} + +.journey-gate-criterion__title { + flex: 1 1 12rem; +} + +.journey-gate-criterion__status { + font-size: 0.85rem; +} + +.journey-gate-map .gate-map-view--embedded { + margin-top: 0.5rem; +} + +.journey-gate-map .gate-map-view--embedded > h2, +.journey-gate-map .gate-map-view--embedded > .section-lead { + display: none; +} + .next-action-embedded { margin-top: 1rem; } @@ -1850,6 +1896,11 @@ color: #047857; } +.status-pill--wait { + background: #fffbeb; + color: #b45309; +} + .status-pill--blocked { background: #fef2f2; color: #b91c1c;