AP1.9d: Methoden-Default-UI und Product Language für Product-Flow.
Some checks failed
Test Suite / lint-backend (push) Waiting to run
Test Suite / compose-smoke (push) Waiting to run
Test Suite / k6 /api/health Baseline (push) Blocked by required conditions
Test Suite / playwright-smoke (push) Blocked by required conditions
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Has been cancelled

Prozessleiste, archetypgebundene Modus-Defaults und Sprint/Arbeitspaket-Begriffe schließen den GUI-Kern für continuous_product vor Sprint-Planungs-Flows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lars 2026-07-12 19:20:06 +02:00
parent 82a5b9adec
commit 09826d146e
20 changed files with 796 additions and 55 deletions

View File

@ -159,4 +159,4 @@ Bei Konflikten:
--- ---
*Siehe auch: `Kairo_Implementation_Truth_Table_v0.1.md` § Referenz-Archetypen, `Kairo_Status_Review_and_Next_Steps_v0.2.md`* *Siehe auch: `Kairo_Implementation_Truth_Table_v0.1.md` § Referenz-Archetypen, `Kairo_Status_Review_and_Next_Steps_v0.2.md`, **`Kairo_Session_Handover_2026-07-12.md`** (Session-Feedback & Stop-Regeln)*

View File

@ -0,0 +1,195 @@
# Jinkendo Kairo
## Session-Handover — 2026-07-12
**Status:** Übergabe an neuen Chat / nächsten Agenten
**Stand:** 2026-07-12 (Abend)
**Branch:** `develop` @ `82a5b9a` (gepusht auf Remote)
**Version (Deploy):** `0.20.2-ap2.2d`
**Vorgänger-Chat:** [MVP-Plan AP2.2 Session](c5b5c664-9bf9-486a-acf9-f20bb005b32e)
---
## 1. Wichtigste Klarstellung
**Es gibt keinen neuen „Plan v0.3“-Bedarf.** Zu Beginn dieser Session wurde der Kurs bereits in **`Kairo_MVP_Execution_Plan_v0.2.md`** festgezogen (PO-Kurskorrektur: Referenz-Archetypen statt Dogfooding-Seed).
Was in der Session schief lief: **Implementierung lief schneller als das Zielbild** — AP2.2b/c/d wurden als Code-Pakete abgearbeitet, während **End-to-End-Flows und methodengebundene Default-UI (AP1.9d)** fehlen. Der Agent schlug am Ende fälschlich vor, den Plan *noch einmal* neu aufzustellen. **Das lehnt der PO ab.**
**Für den nächsten Chat gilt:**
1. **`Kairo_MVP_Execution_Plan_v0.2.md` bleibt führend** — nicht ersetzen, nicht parallel neu erfinden.
2. **Dieses Handover** präzisiert Interpretation, PO-Feedback und **Stop-Regeln** für Implementierung.
3. Nächste Arbeit = **Zielbild-Flows schließen**, nicht weitere Nav-/Hint-Patches.
---
## 2. Session-Verlauf (Kurz)
| Phase | Inhalt |
|-------|--------|
| Start | MVP-Plan fortsetzen; Kurskorrektur: mehrere Referenz-Archetypen anleg- und pflegbar (nicht Dogfooding spiegeln) |
| AP2.2a | Gepusht (`2b0d5dd`): Starter-Kits, geführte Anlage, `seed_004` deprecated |
| AP2.2b/c | Committed + gepusht (`0b57e2e`): A1-Stufenwechsel, Team-Fix, Agent-POST, Steuerungs-Hinweise |
| AP2.2d | Committed + gepusht (`82a5b9a`): Sprint-UI, work_cycle-API im Frontend, Backlog→Sprint-Autoassign |
| PO-Test | UX-Probleme: zu viele Archetypen, UUIDs im Team, fehlende Actor-Anlage, Reifegrad schwer findbar |
| PO-Agile | Zeitbox nicht als Scrum-Kern; Sprint-Planung aus Backlog fehlt; Begriff „Zeitbox“ verwirrend |
| Abschluss | Session dokumentieren, sauberer Handover — **kein weiterer Plan-Reset** |
---
## 3. Was auf `develop` liegt (Commits)
```
82a5b9a AP2.2d: B2b Product-Backlog und B3 Sprint-Backlog in UI.
0b57e2e AP2.2b/c: A1-Stufenwechsel, Team-Fix, Agent-Anlage, Steuerungs-Hinweise.
2b0d5dd AP2.2a: Archetyp-Starter-Kits und MVP Execution Plan v0.2.
```
### AP2.2a (Infrastruktur + Anlage)
- `backend/services/archetype_starter_kit.py` — Kits A1, A2, B2b, B2a
- `GET /api/entity-archetypes/{key}/starter-preview`
- `POST /api/initiatives` mit `apply_starter_kit`
- Frontend: Vorschau in `InitiativeForm.jsx`
### AP2.2b/c (Teil-UI, Backend teils fertig)
- AP2.0e: `maturity_stage_transition.py` — Recurring rotiert bei Stufen-Verify
- Team: `display_name`, `POST /api/actors`, Agent-Formular
- `ArchetypeSteeringHints.jsx`, Optgroups „MVP Stufe A“
### AP2.2d (Teil-UI — **PO: unzureichend für Scrum-Zielbild**)
- `frontend/src/api/workCycles.js`, `WorkCyclesPanel.jsx`
- Plan → **Sprint**, Ausführen → **Sprint**
- Backlog-Convert setzt `work_cycle_id` der aktiven Zeitbox
- `agile_iteration`-Strategie bei aktivem Sprint + `continuous_product`
- **Fehlt:** Sprint-Planung (Backlog auswählen → in Sprint legen), Sprint-Abschluss, optionaler Sprint ohne Zeit-Druck
---
## 4. PO-Feedback (verbindlich für nächste Implementierung)
### 4.1 Allgemein
- **Keine Shortcuts:** Keine Hints/Nav/Redirects als Ersatz für echte methodengebundene Flows.
- **Keine Seeds/Vorbefüllung** als Produkt-Erfahrung; Starter-Kits nur optional beim Anlegen, sonst leeres Vorhaben.
- **GUI muss Prozess zeigen:** Archetyp → Methode → Plan vs. Ist → Kontrolle — nicht sechs lose Plan-Tabs.
- **Abhängigkeiten (Gate-Graph, kritischer Pfad)** bleiben Pflicht (A2, auch unabhängig von Agile).
### 4.2 Agile / Scrum
- **Agile ist Methode** (`agile_iteration`), **kein Archetyp**. Basis: `initiative.product` + `continuous_product`.
- **Sprint ist Planungs-Container**, kein Kalender-Zwang. `work_cycle` im Modell, in UI: **„Sprint“** (nicht „Zeitbox“).
- Gewünschter Flow:
1. Priorisierbares Product Backlog (Eingang)
2. Sprint-Planung anstoßen
3. Items aus Backlog in Sprint legen (Commit)
4. Sprint ausführen
5. Sprint abschließen → Review/Carryover → nächste Planung
- **Aktueller Ist-Stand:** nur „In Maßnahme umwandeln“ ohne Sprint-Auswahl; Ausführen→Sprint leer ohne aktiven Sprint.
### 4.3 Verwirrung in der GUI (Ist)
- Zu viele gleichwertige Plan-Unterpunkte ohne Prozessleiste
- Begriffe: Zeitbox, Maßnahme, Arbeit, Sprint, Eingang — nicht konsistent mit Vision §4 Product Language
- AP1.9d (Methoden-Default-UI) **offen** — überall Listen statt Next Action / methodenspezifischer Default
---
## 5. Execution Plan v0.2 — Interpretations-Korrektur
**Plan bleibt gültig.** Folgende Zeilen sind **überzogen als „✓“ markiert** und sollten im nächsten Chat so gelesen werden:
| Paket | Plan-Status | Tatsächlicher Stand |
|-------|-------------|---------------------|
| AP2.2a | ✓ | Anlage + Starter-Kit — OK als Basis |
| AP2.2b | ◐ | Graph/Strategie da; Kontrolle A2 End-to-End fehlt |
| AP2.2c | ◐ | AP2.0e Backend; Journey/Recurring-UX unvollständig |
| AP2.2d | ✓ (Plan) | **Nur Infrastruktur + Teil-UI** — Sprint-Planungs-Flow fehlt (ADP § Sprint Planning = ○) |
| AP1.9d | offen | **Blocker** für verständliche GUI — vor weiteren AP2.2-Patches priorisieren |
**Nächste Priorität laut Plan (unverändert):** AP1.9d → AP2.2b/c/d **Flows schließen** → AP1.7b → AP2.1 Validation.
**Nicht tun:** Execution Plan v0.3 schreiben, es sei denn PO fordert es explizit.
---
## 6. Zielbild — ein Bild für alle Archetypen
Drei Schichten (aus Vision v0.2 + MVP v0.3):
```text
Archetyp (Was?) → Methode (Wie steuern?) → Plan vs. Ist (Was ist wann?)
```
| Archetyp | Methode | Plan | Ist | Kontrolle |
|----------|---------|------|-----|-----------|
| A2 Linear | `sequential_dependency` | Gate-Graph, Abhängigkeiten | APs an Gates | Nächster ready Schritt am kritischen Pfad |
| A1 Reifegrad | `maturity_progression` | Stufen + Graph | Recurring + APs | Aktive Stufe + heutige Übung |
| B2b Product | `continuous_product` | Eingang (Product Backlog) | Committete APs | Wirkungsvollster Schritt |
| + Agile (Profil) | `agile_iteration` | Sprint-Planung | Sprint-Backlog | Nächster Schritt im Sprint |
Referenz-UI-Konzept (noch PO-Entwurf): `Kairo_PM_Frontend_UI_Concept_v0.1.md` — Modi + Scope + Objekt; **Prozessleiste pro Methode fehlt noch in Implementierung**.
---
## 7. Stop-Regeln für nächsten Agenten
1. **Kein neuer Execution Plan** ohne PO-Freigabe.
2. **Keine weiteren Subnav-Einträge** ohne abgeschlossenen End-to-End-Flow.
3. **Keine Seeds / Dogfooding / Demo-Vorhaben** als Implementierungs-Hebel.
4. **Kein „✓“** in Truth Table / Execution Plan ohne PO-Durchspielbarkeit (Leitfrage ≤2 Min).
5. **Product Language in UI:** Sprint (nicht Zeitbox), Arbeitspaket (nicht Maßnahme wo möglich), Eingang = Product Backlog.
6. **Backend-Tests lokal** oft ohne DB — Verifikation über Push auf `develop` → Pi-Deploy (siehe `.cursor/rules/kairo-deployment-testing.mdc`).
7. **Nicht proaktiv committen/pushen** ohne Nutzeranfrage.
---
## 8. Empfohlener nächster Arbeitsauftrag (für PO-Freigabe)
**Ein zusammenhängendes Paket** — nicht AP2.2x-Fragmente:
### Option A (PO-Priorität GUI-Klarheit): AP1.9d + Product Language
- Methoden-Default pro Modus (Kontrolle/Ausführen/Plan)
- Umbenennung Zeitbox → Sprint in der GUI
- Prozessleiste im Scope bei Product-Vorhaben: Eingang → Sprint planen → Ausführen → Kontrolle
### Option B (Agile-Lücke schließen): Sprint-Planung (Erweiterung AP2.2d, kein neuer Plan)
- Backlog-Items markieren → „In Sprint [R2] planen“
- Sprint-Status: geplant → aktiv → abgeschlossen
- Ausführen ohne aktiven Sprint: Fallback `continuous_product`, nicht leere Sprint-Seite
### Option C (Abhängigkeiten): AP2.2b A2 End-to-End
- Kontrolle: kritischer Pfad + Begründung aus Execution Graph
**PO sollte eine Option wählen** — nicht alles parallel.
---
## 9. Dokumente — Lesereihenfolge nächster Chat
1. **Dieses Handover**
2. `docs/product/Kairo_MVP_Execution_Plan_v0.2.md` (führend, unverändert)
3. `docs/product/Kairo_MVP_Definition_v0.3.md`
4. `docs/product/Kairo_Vision_and_Product_Direction_v0.2.md` §45 (Product Language, Plan vs. Ist)
5. `docs/architecture/ADP_Archetype_and_Method_Catalog_v0.2.md` §2.3, § Sprint-Semantik
6. `docs/product/Kairo_PM_Frontend_UI_Concept_v0.1.md` (UI-Zielbild)
7. `docs/product/Kairo_Implementation_Truth_Table_v0.1.md` (Ist-Check, mit Skepsis gegenüber ✓)
---
## 10. Remote-Verifikation (Pi)
Nach Deploy auf `develop`:
- pytest: `test_ap22a_starter_kit`, `test_ap20e_maturity_transition`, `test_ap22_actor_create`, `test_ap22d_sprint_backlog`, `test_ap20f_work_cycle`
- Manuell: Product anlegen → Eingang → Sprint anlegen → Convert → Ausführen/Sprint
---
## 11. Prompt-Vorschlag für neuen Chat
```text
Lies docs/product/Kairo_Session_Handover_2026-07-12.md und arbeite strikt nach
Kairo_MVP_Execution_Plan_v0.2.md. Kein neuer Plan, keine Seeds, keine Shortcuts.
PO-Priorität: [Option A / B / C aus Handover §8 eintragen].
Zuerst kurz bestätigen, welchen End-to-End-Flow du schließt, dann implementieren.
```
---
*Erstellt am Abschluss der Session 2026-07-12. Execution Plan v0.2 bleibt maßgeblich; dieses Dokument ist Interpretations- und Feedback-Anhang.*

View File

@ -108,7 +108,7 @@ export function ActionForm({
/> />
{cycleOptions.length > 0 && ( {cycleOptions.length > 0 && (
<label> <label>
Sprint / Zeitbox (optional) Sprint (optional)
<select name="work_cycle_id" defaultValue={defaultCycleId}> <select name="work_cycle_id" defaultValue={defaultCycleId}>
<option value=""> kein Sprint </option> <option value=""> kein Sprint </option>
{cycleOptions.map((cycle) => ( {cycleOptions.map((cycle) => (

View File

@ -11,7 +11,7 @@ const HINTS = {
link: { to: '/plan/gates', label: 'Gate-Graph öffnen' }, link: { to: '/plan/gates', label: 'Gate-Graph öffnen' },
}, },
'initiative.product': { 'initiative.product': {
text: 'Product: Eingang triagieren → committen in Sprint-Backlog (aktive Zeitbox).', text: 'Product: Eingang triagieren → in Sprint-Backlog committen.',
link: { to: '/plan/sprint', label: 'Sprint planen' }, link: { to: '/plan/sprint', label: 'Sprint planen' },
secondaryLink: { to: '/work/sprint', label: 'Sprint ausführen' }, secondaryLink: { to: '/work/sprint', label: 'Sprint ausführen' },
}, },

View File

@ -188,7 +188,7 @@ export function BacklogSection({
onClick={() => onConvert(item.id)} onClick={() => onConvert(item.id)}
disabled={busy} disabled={busy}
> >
In Maßnahme umwandeln In Arbeitspaket umwandeln
</button> </button>
)} )}
<button <button

View File

@ -0,0 +1,53 @@
import { NavLink, useLocation } from 'react-router-dom'
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
import { useMethodUiContext } from '../hooks/useMethodUiContext.js'
import { resolveActiveProcessStepKey } from '../config/methodUiDefaults.js'
export function MethodProcessBar() {
const location = useLocation()
const { initiativeId, hrefWithScope } = useProgramScope()
const { processSteps, archetypeKey, loading } = useMethodUiContext()
if (!initiativeId || loading || processSteps.length === 0) {
return null
}
const activeKey = resolveActiveProcessStepKey(location.pathname, processSteps)
return (
<nav
className="method-process-bar"
aria-label={`Prozess — ${archetypeKey || 'Vorhaben'}`}
>
<ol className="method-process-bar__steps">
{processSteps.map((step, index) => {
const isActive = activeKey === step.key
return (
<li
key={step.key}
className={
'method-process-bar__step' +
(isActive ? ' method-process-bar__step--active' : '')
}
>
{index > 0 && (
<span className="method-process-bar__sep" aria-hidden="true">
</span>
)}
<NavLink
to={hrefWithScope(step.to)}
className={({ isActive: routerActive }) =>
'method-process-bar__link' +
(routerActive || isActive ? ' method-process-bar__link--active' : '')
}
>
{step.label}
</NavLink>
</li>
)
})}
</ol>
</nav>
)
}

View File

@ -8,11 +8,13 @@ import { useProgramScope } from '../context/ProgramScopeContext.jsx'
import { buildProjectsByParent } from '../utils/projectTree.js' import { buildProjectsByParent } from '../utils/projectTree.js'
import { projectPath } from '../utils/routes.js' import { projectPath } from '../utils/routes.js'
import { import {
PLAN_OUTLINE_NODES,
resolvePlanOutlineActiveKey, resolvePlanOutlineActiveKey,
resolvePlanOutlineProjectId, resolvePlanOutlineProjectId,
resolvePlanOutlineActionId, resolvePlanOutlineActionId,
} from '../plan/planOutlineNodes.js' } from '../plan/planOutlineNodes.js'
import { resolvePlanOutlineNodes } from '../config/methodUiDefaults.js'
import { getInitiative } from '../api/initiatives.js'
import { getActiveWorkCycle } from '../api/workCycles.js'
import { PlanOutlineWorkTree } from './PlanOutlineWorkTree.jsx' import { PlanOutlineWorkTree } from './PlanOutlineWorkTree.jsx'
function PlanOutlineProjectTree({ projects, parentId, activeProjectId, depth = 0 }) { function PlanOutlineProjectTree({ projects, parentId, activeProjectId, depth = 0 }) {
@ -60,6 +62,8 @@ export function PlanOutlineNav() {
const [projects, setProjects] = useState([]) const [projects, setProjects] = useState([])
const [openActions, setOpenActions] = useState([]) const [openActions, setOpenActions] = useState([])
const [executionGraph, setExecutionGraph] = useState(null) const [executionGraph, setExecutionGraph] = useState(null)
const [archetypeKey, setArchetypeKey] = useState(null)
const [hasActiveSprint, setHasActiveSprint] = useState(false)
const [structureOpen, setStructureOpen] = useState( const [structureOpen, setStructureOpen] = useState(
activeKey === 'structure' || Boolean(activeProjectId), activeKey === 'structure' || Boolean(activeProjectId),
) )
@ -75,6 +79,42 @@ export function PlanOutlineNav() {
setWorkOpen(activeKey === 'work' || Boolean(activeActionId)) setWorkOpen(activeKey === 'work' || Boolean(activeActionId))
}, [activeKey, activeActionId]) }, [activeKey, activeActionId])
useEffect(() => {
if (!initiativeId) {
setArchetypeKey(null)
setHasActiveSprint(false)
return undefined
}
let cancelled = false
Promise.all([
getInitiative(initiativeId),
getActiveWorkCycle(initiativeId).catch(() => null),
])
.then(([initiative, activeCycle]) => {
if (cancelled) return
setArchetypeKey(initiative?.archetype_key || null)
setHasActiveSprint(Boolean(activeCycle?.id))
})
.catch(() => {
if (!cancelled) {
setArchetypeKey(null)
setHasActiveSprint(false)
}
})
return () => {
cancelled = true
}
}, [initiativeId])
const outlineNodes = useMemo(
() =>
resolvePlanOutlineNodes({
archetypeKey,
hasActiveSprint,
}),
[archetypeKey, hasActiveSprint],
)
useEffect(() => { useEffect(() => {
if (!initiativeId) { if (!initiativeId) {
setProjects([]) setProjects([])
@ -149,7 +189,7 @@ export function PlanOutlineNav() {
</div> </div>
<ul className="analysis-split__nav-list"> <ul className="analysis-split__nav-list">
{PLAN_OUTLINE_NODES.map((node) => { {outlineNodes.map((node) => {
const needsInitiative = node.requiresInitiative && !initiativeId const needsInitiative = node.requiresInitiative && !initiativeId
const isDisabled = node.disabled || needsInitiative const isDisabled = node.disabled || needsInitiative
const isStructure = node.key === 'structure' const isStructure = node.key === 'structure'

View File

@ -148,7 +148,7 @@ export function SteeringSnapshotPanel({
{active_work_cycle && ( {active_work_cycle && (
<p className="steering-work-cycle-row"> <p className="steering-work-cycle-row">
<span className="muted">Aktive Zeitbox: </span> <span className="muted">Aktiver Sprint: </span>
<strong>{active_work_cycle.title}</strong> <strong>{active_work_cycle.title}</strong>
{active_work_cycle.goal_description && ( {active_work_cycle.goal_description && (
<span className="steering-method-description muted"> <span className="steering-method-description muted">

View File

@ -31,10 +31,10 @@ export function WorkCyclesPanel({
<section className="card work-cycles-panel"> <section className="card work-cycles-panel">
<div className="section-header"> <div className="section-header">
<div> <div>
<h2>Sprint / Zeitbox</h2> <h2>Sprint</h2>
<p className="section-lead muted"> <p className="section-lead muted">
Aktive Zeitbox definiert den Sprint-Backlog committete Actions mit Zeitbox-Zuweisung Der aktive Sprint definiert den Sprint-Backlog committete Arbeitspakete mit
erscheinen unter Ausführen Sprint. Sprint-Zuweisung erscheinen unter Ausführen Sprint.
</p> </p>
</div> </div>
</div> </div>
@ -50,7 +50,7 @@ export function WorkCyclesPanel({
)} )}
</p> </p>
) : ( ) : (
<p className="muted">Keine aktive Zeitbox alle committeten Actions gelten als Product-Ist.</p> <p className="muted">Kein aktiver Sprint committete Arbeitspakete gelten als Product-Ist.</p>
)} )}
{canManage && ( {canManage && (
@ -82,13 +82,13 @@ export function WorkCyclesPanel({
Sofort aktivieren Sofort aktivieren
</label> </label>
<button type="submit" className="btn btn-primary" disabled={busy}> <button type="submit" className="btn btn-primary" disabled={busy}>
{busy ? 'Anlegen …' : 'Zeitbox anlegen'} {busy ? 'Anlegen …' : 'Sprint anlegen'}
</button> </button>
</form> </form>
)} )}
{workCycles.length === 0 ? ( {workCycles.length === 0 ? (
<EmptyState message="Noch keine Zeitboxen angelegt." /> <EmptyState message="Noch keine Sprints angelegt." />
) : ( ) : (
<ul className="item-list"> <ul className="item-list">
{workCycles.map((cycle) => ( {workCycles.map((cycle) => (

View File

@ -0,0 +1,180 @@
/**
* AP1.9d Methoden-Default-UI: Archetyp/Methode steuert Prozessleiste,
* Modus-Defaults und sichtbare Nav-Knoten (Anti-Todo-Wand).
*
* @typedef {{ key: string, to: string, label: string, mode: 'plan'|'work'|'control' }} ProcessStep
* @typedef {{
* archetypeKey: string | null,
* methodKey: string | null,
* hasActiveSprint: boolean,
* }} MethodUiInput
* @typedef {{
* processSteps: ProcessStep[],
* planDefaultRoute: string,
* workDefaultRoute: string,
* controlDefaultRoute: string,
* planOutlineKeys: string[] | null,
* workNavKeys: string[] | null,
* }} MethodUiDefaults
*/
import { PLAN_OUTLINE_NODES } from '../plan/planOutlineNodes.js'
import { WORK_NAV_ITEMS } from './workNav.js'
const PRODUCT_PROCESS = [
{ key: 'inbox', to: '/plan/inbox', label: 'Eingang', mode: 'plan' },
{ key: 'sprint-plan', to: '/plan/sprint', label: 'Sprint planen', mode: 'plan' },
{ key: 'sprint-work', to: '/work/sprint', label: 'Ausführen', mode: 'work' },
{ key: 'control', to: '/control/status', label: 'Kontrolle', mode: 'control' },
]
const LINEAR_PROCESS = [
{ key: 'gates', to: '/plan/gates', label: 'Zielzustände', mode: 'plan' },
{ key: 'work', to: '/work/today', label: 'Ausführen', mode: 'work' },
{ key: 'control', to: '/control/status', label: 'Kontrolle', mode: 'control' },
]
const MATURITY_PROCESS = [
{ key: 'gates', to: '/plan/gates', label: 'Stufen', mode: 'plan' },
{ key: 'journey', to: '/control/journey', label: 'Rhythmen', mode: 'control' },
{ key: 'work', to: '/work/today', label: 'Ausführen', mode: 'work' },
{ key: 'control', to: '/control/status', label: 'Kontrolle', mode: 'control' },
]
const PROGRAM_PROCESS = [
{ key: 'gates', to: '/plan/gates', label: 'Phasen', mode: 'plan' },
{ key: 'inbox', to: '/plan/inbox', label: 'Eingang', mode: 'plan' },
{ key: 'work', to: '/work/today', label: 'Ausführen', mode: 'work' },
{ key: 'control', to: '/control/status', label: 'Kontrolle', mode: 'control' },
]
const ARCHETYPE_UI = {
'initiative.product': {
processSteps: PRODUCT_PROCESS,
planDefaultRoute: '/plan/inbox',
workDefaultRoute: '/work/sprint',
controlDefaultRoute: '/control/status',
planOutlineKeys: ['profile', 'inbox', 'sprint', 'gates'],
workNavKeys: ['sprint', 'today', 'mine'],
},
'initiative.linear_project': {
processSteps: LINEAR_PROCESS,
planDefaultRoute: '/plan/gates',
workDefaultRoute: '/work/today',
controlDefaultRoute: '/control/status',
planOutlineKeys: ['profile', 'structure', 'gates', 'inbox', 'work'],
workNavKeys: ['today', 'mine', 'sprint'],
},
'initiative.maturity_journey': {
processSteps: MATURITY_PROCESS,
planDefaultRoute: '/plan/gates',
workDefaultRoute: '/work/today',
controlDefaultRoute: '/control/status',
planOutlineKeys: ['profile', 'gates', 'inbox', 'work'],
workNavKeys: ['today', 'mine', 'sprint'],
},
'initiative.program': {
processSteps: PROGRAM_PROCESS,
planDefaultRoute: '/plan/gates',
workDefaultRoute: '/work/today',
controlDefaultRoute: '/control/status',
planOutlineKeys: ['profile', 'structure', 'gates', 'inbox', 'sprint', 'work'],
workNavKeys: ['today', 'mine', 'sprint'],
},
}
const FALLBACK_DEFAULTS = {
processSteps: [],
planDefaultRoute: '/plan/profile',
workDefaultRoute: '/work/today',
controlDefaultRoute: '/control/status',
planOutlineKeys: null,
workNavKeys: null,
}
/**
* @param {MethodUiInput} input
* @returns {MethodUiDefaults}
*/
export function resolveMethodUiDefaults(input = {}) {
const archetypeKey = input.archetypeKey || null
const base = ARCHETYPE_UI[archetypeKey] || FALLBACK_DEFAULTS
let workDefaultRoute = base.workDefaultRoute
if (archetypeKey === 'initiative.product' && !input.hasActiveSprint) {
workDefaultRoute = '/work/today'
}
return {
processSteps: base.processSteps,
planDefaultRoute: base.planDefaultRoute,
workDefaultRoute,
controlDefaultRoute: base.controlDefaultRoute,
planOutlineKeys: base.planOutlineKeys,
workNavKeys: base.workNavKeys,
}
}
/**
* @param {MethodUiInput} input
* @returns {ProcessStep[]}
*/
export function resolveProcessSteps(input = {}) {
return resolveMethodUiDefaults(input).processSteps
}
/**
* @param {MethodUiInput} input
* @returns {typeof PLAN_OUTLINE_NODES}
*/
export function resolvePlanOutlineNodes(input = {}) {
const { planOutlineKeys } = resolveMethodUiDefaults(input)
if (!planOutlineKeys) return PLAN_OUTLINE_NODES
const allowed = new Set(planOutlineKeys)
return PLAN_OUTLINE_NODES.filter((node) => allowed.has(node.key))
}
/**
* @param {MethodUiInput} input
* @returns {typeof WORK_NAV_ITEMS}
*/
export function resolveWorkNavItems(input = {}) {
const { workNavKeys } = resolveMethodUiDefaults(input)
if (!workNavKeys) return WORK_NAV_ITEMS
const order = new Map(workNavKeys.map((key, index) => [key, index]))
return WORK_NAV_ITEMS.filter((item) => order.has(item.key)).sort(
(a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0),
)
}
/**
* Aktiven Prozessschritt aus Modus + Pfad ableiten.
* @param {string} pathname
* @param {ProcessStep[]} steps
*/
export function resolveActiveProcessStepKey(pathname, steps) {
const path = (pathname || '').replace(/\/$/, '') || pathname
if (!steps.length) return null
if (path.startsWith('/control/')) {
const journey = steps.find((s) => s.key === 'journey')
if (journey && path.startsWith('/control/journey')) return 'journey'
return steps.find((s) => s.mode === 'control')?.key ?? null
}
if (path.startsWith('/work/')) {
if (path.startsWith('/work/sprint')) {
return steps.find((s) => s.key === 'sprint-work')?.key ?? null
}
return steps.find((s) => s.key === 'work')?.key ?? null
}
if (path.startsWith('/plan/')) {
if (path.startsWith('/plan/inbox')) return 'inbox'
if (path.startsWith('/plan/sprint')) return 'sprint-plan'
if (path.startsWith('/plan/gates')) return 'gates'
}
if (path.startsWith('/actions/')) {
return steps.find((s) => s.mode === 'work')?.key ?? null
}
return null
}

View File

@ -0,0 +1,68 @@
import { describe, expect, it } from 'vitest'
import {
resolveActiveProcessStepKey,
resolveMethodUiDefaults,
resolvePlanOutlineNodes,
resolveProcessSteps,
resolveWorkNavItems,
} from './methodUiDefaults.js'
describe('methodUiDefaults', () => {
it('product archetype defaults to Eingang and sprint-centric work nav', () => {
const ui = resolveMethodUiDefaults({
archetypeKey: 'initiative.product',
hasActiveSprint: true,
})
expect(ui.planDefaultRoute).toBe('/plan/inbox')
expect(ui.workDefaultRoute).toBe('/work/sprint')
expect(ui.planOutlineKeys).toEqual(['profile', 'inbox', 'sprint', 'gates'])
expect(resolveWorkNavItems({ archetypeKey: 'initiative.product' }).map((i) => i.key)).toEqual([
'sprint',
'today',
'mine',
])
})
it('product without active sprint falls back work default to today', () => {
const ui = resolveMethodUiDefaults({
archetypeKey: 'initiative.product',
hasActiveSprint: false,
})
expect(ui.workDefaultRoute).toBe('/work/today')
})
it('exposes product process bar steps', () => {
const steps = resolveProcessSteps({ archetypeKey: 'initiative.product' })
expect(steps.map((s) => s.label)).toEqual([
'Eingang',
'Sprint planen',
'Ausführen',
'Kontrolle',
])
})
it('linear archetype defaults to gates and filters plan outline', () => {
const ui = resolveMethodUiDefaults({ archetypeKey: 'initiative.linear_project' })
expect(ui.planDefaultRoute).toBe('/plan/gates')
expect(resolvePlanOutlineNodes({ archetypeKey: 'initiative.linear_project' }).map((n) => n.key)).toEqual([
'profile',
'structure',
'gates',
'inbox',
'work',
])
})
it('generic archetype keeps full plan outline and no process steps', () => {
expect(resolveMethodUiDefaults({}).processSteps).toEqual([])
expect(resolvePlanOutlineNodes({}).length).toBe(6)
})
it('resolves active process step from pathname', () => {
const steps = resolveProcessSteps({ archetypeKey: 'initiative.product' })
expect(resolveActiveProcessStepKey('/plan/inbox', steps)).toBe('inbox')
expect(resolveActiveProcessStepKey('/plan/sprint', steps)).toBe('sprint-plan')
expect(resolveActiveProcessStepKey('/work/sprint', steps)).toBe('sprint-work')
expect(resolveActiveProcessStepKey('/control/status', steps)).toBe('control')
})
})

View File

@ -39,23 +39,23 @@ export const PHASE_LABELS = {
export const PHASE_DESCRIPTIONS = { export const PHASE_DESCRIPTIONS = {
capture: 'Ideen und Eingänge erfassen — noch wenig strukturierte Arbeit.', capture: 'Ideen und Eingänge erfassen — noch wenig strukturierte Arbeit.',
triage: 'Backlog wartet auf Commit — noch keine offene nächste Maßnahme.', triage: 'Backlog wartet auf Commit — noch kein nächster Schritt.',
structure: 'Vorhaben braucht Struktur — keine offene Maßnahme, Backlog leer.', structure: 'Vorhaben braucht Struktur — kein offenes Arbeitspaket, Backlog leer.',
commit: 'Maßnahmen committen und zuweisen.', commit: 'Arbeitspakete committen und zuweisen.',
execute: 'Aktive Umsetzung — offene Maßnahmen oder Blocker bearbeiten.', execute: 'Aktive Umsetzung — offene Arbeitspakete oder Blocker bearbeiten.',
verify: 'Nachweise und Ergebnisse belegen.', verify: 'Nachweise und Ergebnisse belegen.',
review: 'Reviews oder Maßnahmen mit Review-Status klären.', review: 'Reviews oder Arbeitspakete mit Review-Status klären.',
adapt: 'Anpassen — Backlog, Meilensteine oder Prioritäten prüfen.', adapt: 'Anpassen — Backlog, Meilensteine oder Prioritäten prüfen.',
closure: 'Vorhaben abgeschlossen oder archiviert.', closure: 'Vorhaben abgeschlossen oder archiviert.',
} }
export const SIGNAL_LABELS = { export const SIGNAL_LABELS = {
initiative_terminal: 'Vorhaben abgeschlossen', initiative_terminal: 'Vorhaben abgeschlossen',
backlog_awaiting_commit: 'Backlog wartet auf Maßnahme', backlog_awaiting_commit: 'Backlog wartet auf Commit',
no_open_work: 'Keine offene Arbeit', no_open_work: 'Keine offene Arbeit',
actions_blocked: 'Maßnahmen blockiert', actions_blocked: 'Arbeitspakete blockiert',
open_blockers: 'Offene Blocker', open_blockers: 'Offene Blocker',
actions_need_review: 'Maßnahmen brauchen Review', actions_need_review: 'Arbeitspakete brauchen Review',
reviews_due: 'Reviews fällig', reviews_due: 'Reviews fällig',
work_in_progress: 'Arbeit in Umsetzung', work_in_progress: 'Arbeit in Umsetzung',
} }
@ -63,19 +63,19 @@ export const SIGNAL_LABELS = {
export const NEXT_ACTION_KIND_LABELS = { export const NEXT_ACTION_KIND_LABELS = {
action: 'Arbeitspaket', action: 'Arbeitspaket',
resolve_blocker: 'Blocker klären', resolve_blocker: 'Blocker klären',
assign_action: 'Maßnahme zuweisen', assign_action: 'Arbeitspaket zuweisen',
convert_backlog: 'Backlog umwandeln', convert_backlog: 'Backlog umwandeln',
create_action: 'Maßnahme anlegen', create_action: 'Arbeitspaket anlegen',
review_milestone: 'Meilenstein prüfen', review_milestone: 'Meilenstein prüfen',
recurring_due: 'Rhythmus fällig', recurring_due: 'Rhythmus fällig',
} }
export const ATTENTION_KIND_LABELS = { export const ATTENTION_KIND_LABELS = {
blocked_action: 'Blockierte Maßnahme', blocked_action: 'Blockiertes Arbeitspaket',
open_blocker: 'Offener Blocker', open_blocker: 'Offener Blocker',
high_priority_action: 'High Priority', high_priority_action: 'High Priority',
unassigned_action: 'Ohne Zuweisung', unassigned_action: 'Ohne Zuweisung',
initiative_without_next_action: 'Keine nächste Maßnahme', initiative_without_next_action: 'Kein nächster Schritt',
stale_initiative: 'Inaktives Vorhaben', stale_initiative: 'Inaktives Vorhaben',
milestone_at_risk: 'Meilenstein gefährdet', milestone_at_risk: 'Meilenstein gefährdet',
gate_graph_blocked: 'Gate blockiert (Graph)', gate_graph_blocked: 'Gate blockiert (Graph)',

View File

@ -0,0 +1,71 @@
import { useEffect, useMemo, useState } from 'react'
import { getInitiative } from '../api/initiatives.js'
import { getActiveWorkCycle } from '../api/workCycles.js'
import { getInitiativeSteeringSnapshot } from '../api/initiatives.js'
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
import { resolveMethodUiDefaults } from '../config/methodUiDefaults.js'
/**
* Lädt Archetyp/Methode für methodengebundene UI (AP1.9d).
*/
export function useMethodUiContext() {
const { initiativeId } = useProgramScope()
const [archetypeKey, setArchetypeKey] = useState(null)
const [methodKey, setMethodKey] = useState(null)
const [hasActiveSprint, setHasActiveSprint] = useState(false)
const [loading, setLoading] = useState(false)
useEffect(() => {
if (!initiativeId) {
setArchetypeKey(null)
setMethodKey(null)
setHasActiveSprint(false)
return undefined
}
let cancelled = false
setLoading(true)
Promise.all([
getInitiative(initiativeId),
getActiveWorkCycle(initiativeId).catch(() => null),
getInitiativeSteeringSnapshot(initiativeId).catch(() => null),
])
.then(([initiative, activeCycle, snapshot]) => {
if (cancelled) return
setArchetypeKey(initiative?.archetype_key || null)
setMethodKey(snapshot?.method_key || null)
setHasActiveSprint(Boolean(activeCycle?.id))
})
.catch(() => {
if (!cancelled) {
setArchetypeKey(null)
setMethodKey(null)
setHasActiveSprint(false)
}
})
.finally(() => {
if (!cancelled) setLoading(false)
})
return () => {
cancelled = true
}
}, [initiativeId])
const input = useMemo(
() => ({ archetypeKey, methodKey, hasActiveSprint }),
[archetypeKey, methodKey, hasActiveSprint],
)
const defaults = useMemo(() => resolveMethodUiDefaults(input), [input])
return {
initiativeId,
archetypeKey,
methodKey,
hasActiveSprint,
loading,
...defaults,
}
}

View File

@ -6,6 +6,7 @@ import { DesktopSidebar } from '../components/DesktopSidebar.jsx'
import { BottomNav } from '../components/BottomNav.jsx' import { BottomNav } from '../components/BottomNav.jsx'
import { ContextIndicator } from '../components/ContextIndicator.jsx' import { ContextIndicator } from '../components/ContextIndicator.jsx'
import { ProgramScopeBar } from '../components/ProgramScopeBar.jsx' import { ProgramScopeBar } from '../components/ProgramScopeBar.jsx'
import { MethodProcessBar } from '../components/MethodProcessBar.jsx'
import { QuickJump } from '../components/QuickJump.jsx' import { QuickJump } from '../components/QuickJump.jsx'
import { RecentlyOpened } from '../components/RecentlyOpened.jsx' import { RecentlyOpened } from '../components/RecentlyOpened.jsx'
@ -43,10 +44,12 @@ export function AppLayout() {
</div> </div>
<ContextIndicator variant="mobile" /> <ContextIndicator variant="mobile" />
<ProgramScopeBar /> <ProgramScopeBar />
<MethodProcessBar />
<RecentlyOpened /> <RecentlyOpened />
</header> </header>
<div className="app-chrome app-chrome--desktop"> <div className="app-chrome app-chrome--desktop">
<ProgramScopeBar /> <ProgramScopeBar />
<MethodProcessBar />
<QuickJump /> <QuickJump />
<RecentlyOpened /> <RecentlyOpened />
</div> </div>

View File

@ -1,13 +1,48 @@
import { Navigate } from 'react-router-dom' import { Navigate } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { useProgramScope } from '../../context/ProgramScopeContext.jsx' import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
import { getInitiative } from '../../api/initiatives.js'
import { getActiveWorkCycle } from '../../api/workCycles.js'
import { resolveMethodUiDefaults } from '../../config/methodUiDefaults.js'
/** Portfolio ohne Scope → Übersicht; mit Vorhaben → Profil als Einstieg. */ /** Portfolio ohne Scope → Übersicht; mit Vorhaben → methoden-Default (AP1.9d). */
export function PlanIndexRedirect() { export function PlanIndexRedirect() {
const { initiativeId, hrefWithScope } = useProgramScope() const { initiativeId, hrefWithScope } = useProgramScope()
const [target, setTarget] = useState(null)
if (initiativeId) { useEffect(() => {
return <Navigate to={hrefWithScope('/plan/profile')} replace /> if (!initiativeId) {
setTarget('/plan/portfolio')
return undefined
}
let cancelled = false
Promise.all([
getInitiative(initiativeId),
getActiveWorkCycle(initiativeId).catch(() => null),
])
.then(([initiative, activeCycle]) => {
if (cancelled) return
const defaults = resolveMethodUiDefaults({
archetypeKey: initiative?.archetype_key,
hasActiveSprint: Boolean(activeCycle?.id),
})
setTarget(hrefWithScope(defaults.planDefaultRoute))
})
.catch(() => {
if (!cancelled) setTarget(hrefWithScope('/plan/profile'))
})
return () => {
cancelled = true
}
}, [initiativeId, hrefWithScope])
if (!initiativeId) {
return <Navigate to="/plan/portfolio" replace />
} }
return <Navigate to="/plan/portfolio" replace /> if (!target) {
return null
}
return <Navigate to={target} replace />
} }

View File

@ -93,7 +93,7 @@ function PlanSprintInner() {
workCycles={workCycles} workCycles={workCycles}
activeWorkCycle={activeWorkCycle} activeWorkCycle={activeWorkCycle}
sectionTitle="Sprint-Backlog" sectionTitle="Sprint-Backlog"
sectionLead="Arbeitspakete in der aktiven Zeitbox — committet aus dem Product Backlog (Eingang) oder direkt hier." sectionLead="Arbeitspakete im aktiven Sprint — committet aus dem Product Backlog (Eingang) oder direkt hier."
/> />
)} )}
</> </>
@ -102,7 +102,7 @@ function PlanSprintInner() {
export function PlanSprintPage() { export function PlanSprintPage() {
return ( return (
<RequireInitiativeScope lead="Sprint-Zeitboxen gehören zu einem Vorhaben im Scope."> <RequireInitiativeScope lead="Sprints gehören zu einem Vorhaben im Scope.">
<ScopedInitiativeProvider> <ScopedInitiativeProvider>
<PlanSprintInner /> <PlanSprintInner />
</ScopedInitiativeProvider> </ScopedInitiativeProvider>

View File

@ -4,23 +4,57 @@ import { ModeAreaShell } from '../../components/ModeAreaShell.jsx'
import { ModeAreaNav } from '../../components/ModeAreaNav.jsx' import { ModeAreaNav } from '../../components/ModeAreaNav.jsx'
import { ModeShell } from '../../components/ModeShell.jsx' import { ModeShell } from '../../components/ModeShell.jsx'
import { WORK_NAV_ITEMS, resolveWorkNavActiveKey } from '../../config/workNav.js' import { WORK_NAV_ITEMS, resolveWorkNavActiveKey } from '../../config/workNav.js'
import { resolveWorkNavItems } from '../../config/methodUiDefaults.js'
import { useProgramScope } from '../../context/ProgramScopeContext.jsx' import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
import { getActiveWorkCycle } from '../../api/workCycles.js'
import { getInitiative } from '../../api/initiatives.js' import { getInitiative } from '../../api/initiatives.js'
import { getActiveWorkCycle } from '../../api/workCycles.js'
import { resolveMethodUiDefaults } from '../../config/methodUiDefaults.js'
function WorkAreaNav() {
const { initiativeId } = useProgramScope()
const [navItems, setNavItems] = useState(WORK_NAV_ITEMS)
useEffect(() => {
if (!initiativeId) {
setNavItems(WORK_NAV_ITEMS)
return undefined
}
let cancelled = false
Promise.all([
getInitiative(initiativeId),
getActiveWorkCycle(initiativeId).catch(() => null),
])
.then(([initiative, activeCycle]) => {
if (cancelled) return
setNavItems(
resolveWorkNavItems({
archetypeKey: initiative?.archetype_key,
hasActiveSprint: Boolean(activeCycle?.id),
}),
)
})
.catch(() => {
if (!cancelled) setNavItems(WORK_NAV_ITEMS)
})
return () => {
cancelled = true
}
}, [initiativeId])
return (
<ModeAreaNav
modeLabel="Ausführen"
items={navItems}
resolveActiveKey={resolveWorkNavActiveKey}
/>
)
}
export function WorkLayout() { export function WorkLayout() {
return ( return (
<div className="app-page"> <div className="app-page">
<ModeShell modeKey="work" /> <ModeShell modeKey="work" />
<ModeAreaShell <ModeAreaShell nav={<WorkAreaNav />}>
nav={
<ModeAreaNav
modeLabel="Ausführen"
items={WORK_NAV_ITEMS}
resolveActiveKey={resolveWorkNavActiveKey}
/>
}
>
<Outlet /> <Outlet />
</ModeAreaShell> </ModeAreaShell>
</div> </div>
@ -28,7 +62,7 @@ export function WorkLayout() {
} }
export function WorkIndexRedirect() { export function WorkIndexRedirect() {
const { initiativeId } = useProgramScope() const { initiativeId, hrefWithScope } = useProgramScope()
const [target, setTarget] = useState('/work/today') const [target, setTarget] = useState('/work/today')
useEffect(() => { useEffect(() => {
@ -37,25 +71,25 @@ export function WorkIndexRedirect() {
return undefined return undefined
} }
let cancelled = false let cancelled = false
Promise.all([getInitiative(initiativeId), getActiveWorkCycle(initiativeId)]) Promise.all([
getInitiative(initiativeId),
getActiveWorkCycle(initiativeId).catch(() => null),
])
.then(([initiative, activeCycle]) => { .then(([initiative, activeCycle]) => {
if (cancelled) return if (cancelled) return
if ( const defaults = resolveMethodUiDefaults({
initiative?.archetype_key === 'initiative.product' && archetypeKey: initiative?.archetype_key,
activeCycle?.id hasActiveSprint: Boolean(activeCycle?.id),
) { })
setTarget(`/work/sprint?initiative=${initiativeId}`) setTarget(hrefWithScope(defaults.workDefaultRoute))
} else {
setTarget(`/work/today?initiative=${initiativeId}`)
}
}) })
.catch(() => { .catch(() => {
if (!cancelled) setTarget('/work/today') if (!cancelled) setTarget(hrefWithScope('/work/today'))
}) })
return () => { return () => {
cancelled = true cancelled = true
} }
}, [initiativeId]) }, [initiativeId, hrefWithScope])
return <Navigate to={target} replace /> return <Navigate to={target} replace />
} }

View File

@ -68,7 +68,7 @@ function WorkSprintInner() {
{!activeWorkCycle && isProductArchetype && ( {!activeWorkCycle && isProductArchetype && (
<section className="card"> <section className="card">
<EmptyState <EmptyState
message="Kein aktiver Sprint — lege unter Plan → Sprint eine Zeitbox an und aktiviere sie." message="Kein aktiver Sprint — lege unter Plan → Sprint einen Sprint an und aktiviere ihn."
/> />
<p> <p>
<Link to={hrefWithScope('/plan/sprint')} className="link-inline"> <Link to={hrefWithScope('/plan/sprint')} className="link-inline">

View File

@ -607,3 +607,65 @@
margin: 0 0 12px; margin: 0 0 12px;
font-size: 13px; font-size: 13px;
} }
/* AP1.9d — methodengebundene Prozessleiste */
.method-process-bar {
width: 100%;
padding: 0 16px 10px;
overflow-x: auto;
}
@media (min-width: 1024px) {
.method-process-bar {
width: auto;
flex: 1 1 100%;
padding: 0;
}
}
.method-process-bar__steps {
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 4px;
margin: 0;
padding: 0;
list-style: none;
font-size: 13px;
}
.method-process-bar__step {
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.method-process-bar__sep {
color: var(--jk-text-subtle);
font-size: 12px;
margin: 0 2px;
}
.method-process-bar__link {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
color: var(--jk-text-muted);
text-decoration: none;
font-weight: 500;
border: 1px solid transparent;
}
.method-process-bar__link:hover {
color: var(--jk-primary);
background: var(--jk-surface-muted, rgba(0, 0, 0, 0.04));
}
.method-process-bar__link--active,
.method-process-bar__step--active .method-process-bar__link {
color: var(--jk-primary);
font-weight: 600;
border-color: var(--jk-primary);
background: var(--jk-surface);
}

View File

@ -6,7 +6,7 @@
const MODE_DEFAULTS = { const MODE_DEFAULTS = {
cockpit: { portfolioHub: '/cockpit', initiativeHub: '/cockpit' }, cockpit: { portfolioHub: '/cockpit', initiativeHub: '/cockpit' },
work: { portfolioHub: '/work/today', initiativeHub: '/work/today' }, work: { portfolioHub: '/work/today', initiativeHub: '/work/today' },
plan: { portfolioHub: '/plan/portfolio', initiativeHub: '/plan/profile' }, plan: { portfolioHub: '/plan/portfolio', initiativeHub: '/plan/inbox' },
control: { portfolioHub: '/control/status', initiativeHub: '/control/status' }, control: { portfolioHub: '/control/status', initiativeHub: '/control/status' },
team: { portfolioHub: '/team', initiativeHub: '/team' }, team: { portfolioHub: '/team', initiativeHub: '/team' },
} }