AP1.6b: Journey um Gate-Fortschritt und Ist-Graph erweitern.
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 2m23s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 13s
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 2m23s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 13s
Fokus-Gates mit Kriterien-Checkliste und eingebetteter Graph-Sicht in Kontrolle; Abnahme-Reihenfolge in Roadmap dokumentiert. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a19c6abd26
commit
b85a6ec0c7
|
|
@ -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`
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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,8 +80,12 @@ export function GateMapView({ initiativeId, items, graphState: graphStateProp })
|
|||
return <ErrorState message={error} />
|
||||
}
|
||||
|
||||
const Wrapper = embedded ? 'div' : 'section'
|
||||
const wrapperClass = embedded ? 'gate-map-view gate-map-view--embedded' : 'card gate-map-view'
|
||||
|
||||
return (
|
||||
<section className="card gate-map-view">
|
||||
<Wrapper className={wrapperClass}>
|
||||
{!embedded && (
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>Zielzustände (Graph)</h2>
|
||||
|
|
@ -91,6 +95,7 @@ export function GateMapView({ initiativeId, items, graphState: graphStateProp })
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="gate-map-view__legend muted">
|
||||
<span className="gate-map-view__legend-item">
|
||||
|
|
@ -209,6 +214,6 @@ export function GateMapView({ initiativeId, items, graphState: graphStateProp })
|
|||
„Abhängigkeiten“ anlegen; ohne Kanten gelten Gates nach Reihenfolge.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
25
frontend/src/components/JourneyGateMapSection.jsx
Normal file
25
frontend/src/components/JourneyGateMapSection.jsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { GateMapView } from './GateMapView.jsx'
|
||||
|
||||
export function JourneyGateMapSection({ initiativeId, items, graphState, loading }) {
|
||||
if (!items.length) return null
|
||||
|
||||
return (
|
||||
<section className="card journey-gate-map">
|
||||
<h2>Zielzustands-Graph (Ist)</h2>
|
||||
<p className="section-lead muted">
|
||||
Baum der Gate-Abhängigkeiten mit Ist-Status — keine Planungsbearbeitung (Kontrolle /
|
||||
Journey).
|
||||
</p>
|
||||
{loading ? (
|
||||
<p className="muted">Graph wird geladen …</p>
|
||||
) : (
|
||||
<GateMapView
|
||||
initiativeId={initiativeId}
|
||||
items={items}
|
||||
graphState={graphState}
|
||||
embedded
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
142
frontend/src/components/JourneyGateProgressSection.jsx
Normal file
142
frontend/src/components/JourneyGateProgressSection.jsx
Normal file
|
|
@ -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 <span className="status-pill status-pill--ready">Bereit</span>
|
||||
}
|
||||
|
||||
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 (
|
||||
<span className={className} title={title}>
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<section className="card journey-gate-progress">
|
||||
<p className="muted">Zielzustands-Fortschritt wird geladen …</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
if (!items.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const focusGates = sortGatesForJourney(items, graphState).slice(0, 5)
|
||||
|
||||
return (
|
||||
<section className="card journey-gate-progress">
|
||||
<h2>Zielzustands-Fortschritt</h2>
|
||||
<p className="section-lead muted">
|
||||
Nächste Gates und Kriterien im Ist — Schritt für Schritt bis Verify. Vollständige Pflege
|
||||
auf der Gate-Detailseite.
|
||||
</p>
|
||||
<ul className="item-list journey-gate-progress-list">
|
||||
{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 (
|
||||
<li key={gate.id} className="list-item card-list-item journey-gate-progress-item">
|
||||
<div className="list-item-main">
|
||||
<div className="journey-gate-progress-item__head">
|
||||
<Link to={gatePath(gate.id)} className="link-button">
|
||||
<strong>{gate.title}</strong>
|
||||
</Link>
|
||||
<StatusBadge kind="milestone" status={gate.status} />
|
||||
<GateSteeringHint
|
||||
graphState={graphState}
|
||||
itemId={gate.id}
|
||||
siblingItems={items}
|
||||
/>
|
||||
</div>
|
||||
{progress?.total > 0 && (
|
||||
<p className="muted list-item-sub">
|
||||
Kriterien: {progress.closed}/{progress.total} erfüllt
|
||||
</p>
|
||||
)}
|
||||
{criteria.length > 0 && (
|
||||
<ol className="journey-gate-criteria-list">
|
||||
{criteria.map((criterion) => (
|
||||
<li
|
||||
key={criterion.id}
|
||||
className={
|
||||
'journey-gate-criterion' +
|
||||
(['satisfied', 'waived', 'deferred'].includes(criterion.status)
|
||||
? ' journey-gate-criterion--done'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
<span className="journey-gate-criterion__title">{criterion.title}</span>
|
||||
<span className="muted journey-gate-criterion__status">
|
||||
{CRITERION_STATUS_LABELS[criterion.status] || criterion.status}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
{openCriteria.length === 0 && criteria.length > 0 && gate.status !== 'reached' && (
|
||||
<p className="muted list-item-sub">Alle Kriterien bearbeitet — Verify prüfen.</p>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 && <p className="list-item-desc muted">{event.summary}</p>}
|
||||
<div className="journey-event-links">
|
||||
{event.roadmap_item_id && gateTitles[event.roadmap_item_id] && (
|
||||
<Link
|
||||
to={`/initiatives/${initiativeId}/plan/items/${event.roadmap_item_id}`}
|
||||
className="link-button"
|
||||
>
|
||||
<Link to={gatePath(event.roadmap_item_id)} className="link-button">
|
||||
Gate: {gateTitles[event.roadmap_item_id]}
|
||||
</Link>
|
||||
)}
|
||||
{event.kind === 'action' && (
|
||||
<Link
|
||||
to={`/initiatives/${initiativeId}/actions/${event.entity_id}`}
|
||||
className="link-button"
|
||||
>
|
||||
<Link to={actionPath(event.entity_id)} className="link-button">
|
||||
Arbeitspaket öffnen
|
||||
</Link>
|
||||
)}
|
||||
{event.kind === 'gate' && (
|
||||
<Link
|
||||
to={`/initiatives/${initiativeId}/plan/items/${event.entity_id}`}
|
||||
className="link-button"
|
||||
>
|
||||
<Link to={gatePath(event.entity_id)} className="link-button">
|
||||
Gate öffnen
|
||||
</Link>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 && <p className="error">{error}</p>}
|
||||
|
||||
<JourneyGateProgressSection
|
||||
items={roadmapItems}
|
||||
graphState={graphState}
|
||||
criteriaProgress={criteriaProgress}
|
||||
criteriaByGate={criteriaByGate}
|
||||
loading={gateMetaLoading}
|
||||
/>
|
||||
|
||||
<JourneyGateMapSection
|
||||
initiativeId={id}
|
||||
items={roadmapItems}
|
||||
graphState={graphState}
|
||||
loading={gateMetaLoading}
|
||||
/>
|
||||
|
||||
<section className="card journey-timeline-section">
|
||||
<h2>Journey</h2>
|
||||
<p className="section-lead muted">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user