AP2.3f: Restschuld ? uiFeatures, Route-Registry und slice-bewusste Mutationen.
Some checks failed
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Failing after 3m9s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 3s
Test Suite / compose-smoke (push) Has been skipped

Archetyp-Profile und Seitenlogik auf Operating Context migriert; methodUiDefaults nur noch Fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lars 2026-07-25 10:52:12 +02:00
parent 44fda7beb9
commit c79bfa844f
16 changed files with 432 additions and 300 deletions

View File

@ -1,4 +1,4 @@
"""UI-Profile für Initiative-Archetypen — migriert aus methodUiDefaults.js (AP2.3a)."""
"""UI-Profile für Initiative-Archetypen — AP2.3a / AP2.3f."""
from __future__ import annotations
@ -37,6 +37,17 @@ _SUPPORT_QUEUE_PROCESS = (
{"key": "control", "to": "/control/status", "label": "Kontrolle", "mode": "control"},
)
_CONTENT_PROCESS = (
{"key": "gates", "to": "/plan/gates", "label": "Kapitel", "mode": "plan"},
{"key": "work", "to": "/work/today", "label": "Ausführen", "mode": "work"},
{"key": "control", "to": "/control/status", "label": "Kontrolle", "mode": "control"},
)
_DISPUTE_PROCESS = (
{"key": "work", "to": "/work/today", "label": "Ausführen", "mode": "work"},
{"key": "control", "to": "/control/status", "label": "Kontrolle", "mode": "control"},
)
_COMMON_OM_SLICES = (
"actions",
"blockers",
@ -53,15 +64,25 @@ GENERIC_UI_PROFILE: dict[str, Any] = {
"controlDefaultRoute": "/control/status",
"planOutlineKeys": None,
"workNavKeys": None,
"dataSlices": ["actions", "backlog", "roadmap", "projects", "blockers", *_COMMON_OM_SLICES[1:]],
"dataSlices": [
"actions",
"backlog",
"roadmap",
"projects",
"blockers",
*_COMMON_OM_SLICES[1:],
],
"enabledModes": ["plan", "work", "control"],
"uiFeatures": {},
}
INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
"initiative.generic": dict(GENERIC_UI_PROFILE),
"initiative.product": {
"processSteps": list(_PRODUCT_PROCESS),
"planDefaultRoute": "/plan/inbox",
"workDefaultRoute": "/work/sprint",
"workDefaultRouteWithoutActiveWorkCycle": "/work/today",
"controlDefaultRoute": "/control/status",
"planOutlineKeys": ["profile", "inbox", "sprint", "gates"],
"workNavKeys": ["sprint", "today", "mine"],
@ -78,6 +99,10 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
"steering_methods",
],
"enabledModes": ["plan", "work", "control"],
"uiFeatures": {
"continuousProductWorkMode": True,
"steeringSnapshotOnWorkSprint": True,
},
},
"initiative.linear_project": {
"processSteps": list(_LINEAR_PROCESS),
@ -98,6 +123,9 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
"steering_methods",
],
"enabledModes": ["plan", "work", "control"],
"uiFeatures": {
"criticalPathControl": True,
},
},
"initiative.maturity_journey": {
"processSteps": list(_MATURITY_PROCESS),
@ -117,6 +145,7 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
"steering_methods",
],
"enabledModes": ["plan", "work", "control"],
"uiFeatures": {},
},
"initiative.program": {
"processSteps": list(_PROGRAM_PROCESS),
@ -138,6 +167,7 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
"steering_methods",
],
"enabledModes": ["plan", "work", "control"],
"uiFeatures": {},
},
"initiative.support_queue": {
"processSteps": list(_SUPPORT_QUEUE_PROCESS),
@ -153,6 +183,7 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
"steering_methods",
],
"enabledModes": ["plan", "work", "control"],
"uiFeatures": {},
},
"initiative.recurring_program": {
"processSteps": list(_MATURITY_PROCESS),
@ -169,6 +200,44 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
"steering_methods",
],
"enabledModes": ["plan", "work", "control"],
"uiFeatures": {},
},
"initiative.content_project": {
"processSteps": list(_CONTENT_PROCESS),
"planDefaultRoute": "/plan/gates",
"workDefaultRoute": "/work/today",
"controlDefaultRoute": "/control/status",
"planOutlineKeys": ["profile", "gates", "inbox", "work"],
"workNavKeys": ["today", "mine"],
"dataSlices": [
"actions",
"roadmap",
"blockers",
"evidence",
"decisions",
"reviews",
"steering_methods",
],
"enabledModes": ["plan", "work", "control"],
"uiFeatures": {},
},
"initiative.dispute_case": {
"processSteps": list(_DISPUTE_PROCESS),
"planDefaultRoute": "/plan/profile",
"workDefaultRoute": "/work/today",
"controlDefaultRoute": "/control/status",
"planOutlineKeys": ["profile", "work"],
"workNavKeys": ["today", "mine"],
"dataSlices": [
"actions",
"blockers",
"evidence",
"decisions",
"reviews",
"steering_methods",
],
"enabledModes": ["plan", "work", "control"],
"uiFeatures": {},
},
}

View File

@ -58,6 +58,16 @@ def register() -> None:
label="Verfahren / Konflikt",
description="Reaktive Steuerung, Fristen, Entscheidungen",
next_action_strategy_key="default",
data_slices=frozenset(
{
"actions",
"blockers",
"evidence",
"decisions",
"reviews",
"steering_methods",
}
),
compatible_archetype_keys=frozenset({"initiative.dispute_case"}),
)
register_stub_method(
@ -65,5 +75,16 @@ def register() -> None:
label="Kapitel-Entwicklung",
description="Inhaltliche Progression, Reviews",
next_action_strategy_key="default",
data_slices=frozenset(
{
"actions",
"roadmap",
"blockers",
"evidence",
"decisions",
"reviews",
"steering_methods",
}
),
compatible_archetype_keys=frozenset({"initiative.content_project"}),
)

View File

@ -84,6 +84,8 @@ Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden.
| Situativer Steuerungskontext (Next Action) | ✗ | 📄 Vision §7.6; AP1.8b deferred |
| AttentionItem | ◐ | |
| Initiative Steering Snapshot | ✓ | Actions + linked; Archetyp/Guidance AP2.0c |
| Operating Context API | ✓ | AP2.3a: `GET …/operating-context`, `ui_profile_json` Migration 027 |
| Archetyp-/Methoden-Plugin-Architektur | ◐ | AP2.3 AE ✓; AP2.3f Restschuld (Resolver, Route-Gating, uiFeatures, slice reload) ◐ |
| operating_phase | ✗ | entfernt AP1.2 |
| signals (Snapshot) | ✓ | `snapshot_signals.py` |
| Graph Read Models (blocked/ready) | ◐ | AP1.4d/e; Join/OR AP1.15d deferred |
@ -117,6 +119,8 @@ Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden.
| Plan-Outline | ◐ | AP1.12ad: Baum, Modal, Reorder, Actions; AP-Kanten AP1.16c offen |
| Execution-Plan (AP-Graph) | ◐ | AP1.16c: Outline + Arbeit-Liste + Action-Detail |
| Profil-Modal (Archetyp/EFS) | ◐ | AP1.10c |
| Operating Profile Resolver (FE) | ◐ | AP2.3b/f: API-first, `methodUiDefaults` Fallback |
| Route Gating (Modus-Routen) | ◐ | AP2.3d/f: `OperatingRouteGate`, `MODE_ROUTE_GROUPS` |
| Modal-Bearbeitung (Gates/Profil) | ◐ | viele Sektionen noch Inline-CRUD |
| Admin-UI | ✗ | |
| Team-Modus | ✗ | AP1.9f |

View File

@ -4,7 +4,8 @@
**Status:** PO-Kurskorrektur — **führend für Implementierung ab 2026-07-12**
**Stand:** 2026-07-12
**Ersetzt als Priorisierung:** Dogfooding-Seed als Implementierungs-Hebel; Status-Review §5 (Dogfooding-first)
**Bezug:** `Kairo_MVP_Definition_v0.3.md` §59, `ADP_Archetype_and_Method_Catalog_v0.2.md` §3, `Kairo_Corrected_MVP_Roadmap_v0.2.md` (AP-Historie)
**Bezug:** `Kairo_MVP_Definition_v0.3.md` §59, `ADP_Archetype_and_Method_Catalog_v0.2.md` §3, `Kairo_Corrected_MVP_Roadmap_v0.2.md` (AP-Historie)
**Spezifikationsphase (ab 2026-07-24):** `Kairo_Archetype_Specification_Program_v0.1.md` + `docs/product/archetypes/` — Spec vor neuem Archetyp-Code (Welle 2+); ersetzt diesen Plan nicht
---

View File

@ -15,9 +15,8 @@ import {
PlanIndexRedirect,
ControlLayout,
ControlIndexRedirect,
MODE_ROUTE_COMPONENTS,
} from './registry/viewRegistry.js'
import { OperatingRouteGate } from './registry/OperatingRouteGate.jsx'
import { renderModeRoutes } from './registry/renderModeRoutes.jsx'
import {
WorkspaceRedirect,
MyActionsRedirect,
@ -65,90 +64,17 @@ function AppRoutes() {
<Route path="/work" element={<WorkLayout />}>
<Route index element={<WorkIndexRedirect />} />
<Route
path="today"
element={
<OperatingRouteGate>
<MODE_ROUTE_COMPONENTS.workToday />
</OperatingRouteGate>
}
/>
<Route
path="sprint"
element={
<OperatingRouteGate>
<MODE_ROUTE_COMPONENTS.workSprint />
</OperatingRouteGate>
}
/>
<Route
path="mine"
element={
<OperatingRouteGate>
<MODE_ROUTE_COMPONENTS.workMine />
</OperatingRouteGate>
}
/>
{renderModeRoutes('work')}
</Route>
<Route path="/plan" element={<PlanLayout />}>
<Route index element={<PlanIndexRedirect />} />
<Route path="portfolio" element={<MODE_ROUTE_COMPONENTS.planPortfolio />} />
<Route path="profile" element={<MODE_ROUTE_COMPONENTS.planProfile />} />
<Route
path="structure"
element={
<OperatingRouteGate>
<MODE_ROUTE_COMPONENTS.planStructure />
</OperatingRouteGate>
}
/>
<Route
path="gates"
element={
<OperatingRouteGate>
<MODE_ROUTE_COMPONENTS.planGates />
</OperatingRouteGate>
}
/>
<Route
path="inbox"
element={
<OperatingRouteGate>
<MODE_ROUTE_COMPONENTS.planInbox />
</OperatingRouteGate>
}
/>
<Route
path="sprint"
element={
<OperatingRouteGate>
<MODE_ROUTE_COMPONENTS.planSprint />
</OperatingRouteGate>
}
/>
<Route path="work" element={<MODE_ROUTE_COMPONENTS.planWork />} />
{renderModeRoutes('plan')}
</Route>
<Route path="/control" element={<ControlLayout />}>
<Route index element={<ControlIndexRedirect />} />
<Route path="status" element={<MODE_ROUTE_COMPONENTS.controlStatus />} />
<Route
path="plan-ist"
element={
<OperatingRouteGate>
<MODE_ROUTE_COMPONENTS.controlPlanIst />
</OperatingRouteGate>
}
/>
<Route
path="journey"
element={
<OperatingRouteGate>
<MODE_ROUTE_COMPONENTS.controlJourney />
</OperatingRouteGate>
}
/>
{renderModeRoutes('control')}
</Route>
<Route path="/projects/:projectId" element={<ProjectObjectPage />} />

View File

@ -91,6 +91,7 @@ import {
applySliceResults,
loadOperatingSlices,
} from '../registry/operatingSliceLoaders.js'
import { resolveOperatingProfileFromInput } from '../registry/resolveOperatingProfile.js'
const InitiativeOperationsContext = createContext(null)
@ -158,6 +159,22 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
[id, sliceSetters],
)
const refreshOm = useCallback(
async (...sliceKeys) => {
if (!id || !sliceKeys.length) return
const unique = [...new Set(sliceKeys.flat())]
await reloadSlices(unique)
},
[id, reloadSlices],
)
const refreshOperatingContext = useCallback(async () => {
if (!id) return null
const opCtx = await getInitiativeOperatingContext(id).catch(() => null)
setOperatingContext(opCtx)
return opCtx
}, [id])
const load = useCallback(async ({ silent = false, sliceKeys = null } = {}) => {
if (sessionLoading || !id) return
if (!silent) setLoading(true)
@ -319,7 +336,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
setMethodBusy(true)
try {
await updateInitiativeSteeringMethod(id, methodKey)
setSteeringSnapshot(await getInitiativeSteeringSnapshot(id))
await refreshOperatingContext()
await refreshOm(['steering_snapshot', 'steering_methods'])
} catch (err) {
setSteeringSnapshotError(err.message)
} finally {
@ -331,7 +349,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
setMethodBusy(true)
try {
await updateInitiativeMethodProfile(id, methodProfileKey || null)
setSteeringSnapshot(await getInitiativeSteeringSnapshot(id))
await refreshOperatingContext()
await refreshOm(['steering_snapshot', 'steering_methods'])
} catch (err) {
setSteeringSnapshotError(err.message)
} finally {
@ -354,7 +373,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
assigned_actor_ids: assigned,
})
setShowActionForm(false)
await load()
await refreshOm(['actions'])
} catch (err) {
setError(err.message)
} finally {
@ -383,7 +402,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
await setActionAssignments(actionId, payload.assigned_actor_ids || [])
}
setEditingActionId(null)
await load()
await refreshOm(['actions'])
} catch (err) {
setError(err.message)
} finally {
@ -410,7 +429,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
if (!capabilities.has('kairo.action.manage')) return
try {
await updateAction(action.id, { status })
await load()
await refreshOm(['actions'])
} catch (err) {
setError(err.message)
}
@ -420,7 +439,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
setFormBusy(true)
try {
await createInitiativeBlocker(id, body)
await load()
await refreshOm(['blockers', 'actions'])
} catch (err) {
setError(err.message)
} finally {
@ -437,7 +456,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
action_id: actionId,
set_action_blocked: true,
})
await load()
await refreshOm(['blockers', 'actions'])
} catch (err) {
setError(err.message)
} finally {
@ -448,7 +467,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
async function handleBlockerStatus(blockerId, status) {
try {
await updateBlocker(blockerId, { status })
await load()
await refreshOm(['blockers', 'actions'])
} catch (err) {
setError(err.message)
}
@ -457,7 +476,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
async function handleDeleteBlocker(blockerId) {
try {
await deleteBlocker(blockerId)
await load()
await refreshOm(['blockers', 'actions'])
} catch (err) {
setError(err.message)
}
@ -469,7 +488,11 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
try {
await updateInitiative(id, core)
await saveInitiativeDynamicFields(id, dynamicFields)
await load()
await refreshOm(['initiative'])
if (core.archetype_key) {
await refreshOperatingContext()
await load({ silent: true })
}
return true
} catch (err) {
setError(err.message)
@ -483,7 +506,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
setFormBusy(true)
try {
await createInitiativeBacklogItem(id, body)
await load()
await refreshOm(['backlog'])
} catch (err) {
setError(err.message)
} finally {
@ -495,7 +518,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
setFormBusy(true)
try {
await updateBacklogItem(itemId, body)
await load()
await refreshOm(['backlog'])
return true
} catch (err) {
setError(err.message)
@ -511,7 +534,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
roadmap_item_id: roadmapItemId || undefined,
clear_roadmap_item: !roadmapItemId,
})
await load()
await refreshOm(['backlog'])
} catch (err) {
setError(err.message)
}
@ -520,7 +543,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
async function handleBacklogStatus(itemId, status) {
try {
await updateBacklogItem(itemId, { status })
await load()
await refreshOm(['backlog'])
} catch (err) {
setError(err.message)
}
@ -587,7 +610,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
setFormBusy(true)
try {
const created = await createWorkCycle(id, body)
await load({ silent: true })
await refreshOm(['work_cycles', 'actions'])
if (created?.id) setSelectedWorkCycleId(created.id)
} catch (err) {
setError(err.message)
@ -600,7 +623,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
setFormBusy(true)
try {
await activateWorkCycle(id, workCycleId)
await load({ silent: true })
await refreshOm(['work_cycles', 'actions'])
setSelectedWorkCycleId(workCycleId)
} catch (err) {
setError(err.message)
@ -613,7 +636,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
setFormBusy(true)
try {
await completeWorkCycle(id, workCycleId)
await load({ silent: true })
await refreshOm(['work_cycles', 'actions'])
} catch (err) {
setError(err.message)
} finally {
@ -635,7 +658,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
}
}
await completeWorkCycle(id, workCycleId)
await load({ silent: true })
await refreshOm(['work_cycles', 'actions'])
} catch (err) {
setError(err.message)
throw err
@ -648,7 +671,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
setFormBusy(true)
try {
await reactivateWorkCycle(id, workCycleId)
await load({ silent: true })
await refreshOm(['work_cycles', 'actions'])
setSelectedWorkCycleId(workCycleId)
} catch (err) {
setError(err.message)
@ -660,7 +683,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
async function handleDeleteBacklog(itemId) {
try {
await deleteBacklogItem(itemId)
await load()
await refreshOm(['backlog'])
} catch (err) {
setError(err.message)
}
@ -670,7 +693,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
setFormBusy(true)
try {
const created = await createInitiativeRoadmapItem(id, body)
await load()
await refreshOm(['roadmap'])
return created
} catch (err) {
setError(err.message)
@ -683,7 +706,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
async function handleRoadmapItemStatus(itemId, status) {
try {
await updateRoadmapItem(itemId, { status })
await load()
await refreshOm(['roadmap'])
} catch (err) {
setError(err.message)
}
@ -692,7 +715,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
async function handleUpdateRoadmapItem(itemId, body) {
try {
await updateRoadmapItem(itemId, body)
await load()
await refreshOm(['roadmap'])
} catch (err) {
setError(err.message)
}
@ -708,7 +731,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
status: 'accepted',
})
await verifyRoadmapItemReached(itemId)
await load()
await refreshOm(['roadmap', 'steering_snapshot'])
} catch (err) {
setError(err.message)
} finally {
@ -720,7 +743,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
setFormBusy(true)
try {
await verifyRoadmapItemReached(itemId)
await load()
await refreshOm(['roadmap', 'steering_snapshot'])
} catch (err) {
setError(err.message)
} finally {
@ -731,7 +754,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
async function handleDeleteRoadmapItem(itemId) {
try {
await deleteRoadmapItem(itemId)
await load()
await refreshOm(['roadmap'])
} catch (err) {
setError(err.message)
}
@ -812,7 +835,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
await Promise.all(
patches.map((patch) => updateBacklogItem(patch.id, { sort_order: patch.sort_order })),
)
await load()
await refreshOm(['backlog'])
return true
} catch (err) {
setError(err.message)
@ -830,7 +853,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
await Promise.all(
patches.map((patch) => updateRoadmapItem(patch.id, { sort_order: patch.sort_order })),
)
await load()
await refreshOm(['roadmap'])
return true
} catch (err) {
setError(err.message)
@ -849,7 +872,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
setFormBusy(true)
try {
await createInitiativeEvidence(id, body)
await load()
await refreshOm(['evidence'])
} catch (err) {
setError(err.message)
} finally {
@ -860,7 +883,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
async function handleEvidenceStatus(evidenceId, status) {
try {
await updateEvidence(evidenceId, { status })
await load()
await refreshOm(['evidence'])
} catch (err) {
setError(err.message)
}
@ -869,7 +892,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
async function handleDeleteEvidence(evidenceId) {
try {
await deleteEvidence(evidenceId)
await load()
await refreshOm(['evidence'])
} catch (err) {
setError(err.message)
}
@ -879,7 +902,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
setFormBusy(true)
try {
await createInitiativeDecision(id, body)
await load()
await refreshOm(['decisions'])
} catch (err) {
setError(err.message)
} finally {
@ -890,7 +913,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
async function handleDecisionStatus(decisionId, status) {
try {
await updateDecision(decisionId, { status })
await load()
await refreshOm(['decisions'])
} catch (err) {
setError(err.message)
}
@ -899,7 +922,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
async function handleDeleteDecision(decisionId) {
try {
await deleteDecision(decisionId)
await load()
await refreshOm(['decisions'])
} catch (err) {
setError(err.message)
}
@ -909,7 +932,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
setFormBusy(true)
try {
await createInitiativeReview(id, body)
await load()
await refreshOm(['reviews'])
} catch (err) {
setError(err.message)
} finally {
@ -920,7 +943,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
async function handleReviewStatus(reviewId, status) {
try {
await updateReview(reviewId, { status })
await load()
await refreshOm(['reviews'])
} catch (err) {
setError(err.message)
}
@ -929,7 +952,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
async function handleDeleteReview(reviewId) {
try {
await deleteReview(reviewId)
await load()
await refreshOm(['reviews'])
} catch (err) {
setError(err.message)
}
@ -939,7 +962,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
setFormBusy(true)
try {
await createInitiativeRecurring(id, body)
await load()
await refreshOm(['recurring'])
} catch (err) {
setError(err.message)
} finally {
@ -950,7 +973,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
async function handleRecurringStatus(recurringId, status) {
try {
await updateRecurring(recurringId, { status })
await load()
await refreshOm(['recurring'])
} catch (err) {
setError(err.message)
}
@ -959,12 +982,23 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
async function handleDeleteRecurring(recurringId) {
try {
await deleteRecurring(recurringId)
await load()
await refreshOm(['recurring'])
} catch (err) {
setError(err.message)
}
}
const operatingProfile = useMemo(
() =>
resolveOperatingProfileFromInput({
operatingContext,
hasActiveSprint: Boolean(activeWorkCycle?.id),
}),
[operatingContext, activeWorkCycle?.id],
)
const uiFeatures = operatingProfile.uiFeatures || {}
const value = {
initiativeId: id,
initiative,
@ -996,6 +1030,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
steeringMethods,
operatingContext,
dataSlices: operatingContext?.data_slices || [],
uiFeatures,
operatingProfile,
methodBusy,
loading,
error,
@ -1015,6 +1051,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
reloadActors: actorsState.reload,
reload: load,
reloadSlices,
refreshOm,
refreshOperatingContext,
handleMethodChange,
handleMethodProfileChange,
handleCreateAction,

View File

@ -5,14 +5,6 @@ import { SteeringSnapshotPanel } from '../../components/SteeringSnapshotPanel.js
import { CriticalPathPanel } from '../../components/CriticalPathPanel.jsx'
import { NextActionWidget } from '../../widgets/NextActionWidget.jsx'
function usesCriticalPathControl(initiative, steeringSnapshot) {
const archetypeKey = initiative?.archetype_key
const methodKey = steeringSnapshot?.method_key
return (
archetypeKey === 'initiative.linear_project' || methodKey === 'sequential_dependency'
)
}
export function InitiativeOverviewPage() {
const {
initiativeId,
@ -26,10 +18,11 @@ export function InitiativeOverviewPage() {
methodBusy,
capabilities,
handleMethodChange,
uiFeatures,
} = useInitiativeOperations()
const counts = steeringSnapshot?.counts || {}
const showCriticalPath = usesCriticalPathControl(initiative, steeringSnapshot)
const showCriticalPath = Boolean(uiFeatures.criticalPathControl)
return (
<>

View File

@ -6,6 +6,8 @@ import {
CONTROL_NAV_ITEMS,
resolveControlNavActiveKey,
} from '../../config/controlNav.js'
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
import { useMethodUiContext } from '../../hooks/useMethodUiContext.js'
export function ControlLayout() {
return (
@ -29,5 +31,13 @@ export function ControlLayout() {
}
export function ControlIndexRedirect() {
return <Navigate to="/control/status" replace />
const { initiativeId, hrefWithScope } = useProgramScope()
const { controlDefaultRoute, loading, operatingContext } = useMethodUiContext()
if (initiativeId && loading && !operatingContext) {
return null
}
const target = initiativeId ? hrefWithScope(controlDefaultRoute) : '/control/status'
return <Navigate to={target} replace />
}

View File

@ -1,48 +1,19 @@
import { useEffect, useState } from 'react'
import { Navigate } from 'react-router-dom'
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
import { getInitiativeOperatingContext } from '../../api/initiatives.js'
import { getActiveWorkCycle } from '../../api/workCycles.js'
import { resolveOperatingProfileFromInput } from '../../registry/resolveOperatingProfile.js'
import { useMethodUiContext } from '../../hooks/useMethodUiContext.js'
/** Portfolio ohne Scope → Übersicht; mit Vorhaben → methoden-Default (AP1.9d). */
/** Portfolio ohne Scope → Übersicht; mit Vorhaben → methoden-Default aus Operating Context. */
export function PlanIndexRedirect() {
const { initiativeId, hrefWithScope } = useProgramScope()
const [target, setTarget] = useState(null)
useEffect(() => {
if (!initiativeId) {
setTarget('/plan/portfolio')
return undefined
}
let cancelled = false
Promise.all([
getInitiativeOperatingContext(initiativeId),
getActiveWorkCycle(initiativeId).catch(() => null),
])
.then(([context, activeCycle]) => {
if (cancelled) return
const defaults = resolveOperatingProfileFromInput({
operatingContext: context,
hasActiveSprint: Boolean(activeCycle?.id),
})
setTarget(hrefWithScope(defaults.planDefaultRoute))
})
.catch(() => {
if (!cancelled) setTarget(hrefWithScope('/plan/profile'))
})
return () => {
cancelled = true
}
}, [initiativeId, hrefWithScope])
const { planDefaultRoute, loading, operatingContext } = useMethodUiContext()
if (!initiativeId) {
return <Navigate to="/plan/portfolio" replace />
}
if (!target) {
if (loading && !operatingContext) {
return null
}
return <Navigate to={target} replace />
return <Navigate to={hrefWithScope(planDefaultRoute)} replace />
}

View File

@ -1,45 +1,22 @@
import { Navigate, Outlet } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { useMemo } from 'react'
import { ModeAreaShell } from '../../components/ModeAreaShell.jsx'
import { ModeAreaNav } from '../../components/ModeAreaNav.jsx'
import { ModeShell } from '../../components/ModeShell.jsx'
import { WORK_NAV_ITEMS, resolveWorkNavActiveKey } from '../../config/workNav.js'
import { resolveWorkNavItems } from '../../config/methodUiDefaults.js'
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
import { getInitiative } from '../../api/initiatives.js'
import { getActiveWorkCycle } from '../../api/workCycles.js'
import { resolveMethodUiDefaults } from '../../config/methodUiDefaults.js'
import { useMethodUiContext } from '../../hooks/useMethodUiContext.js'
import { resolveWorkNavItemsFromProfile } from '../../registry/resolveOperatingProfile.js'
function WorkAreaNav() {
const { initiativeId } = useProgramScope()
const [navItems, setNavItems] = useState(WORK_NAV_ITEMS)
const { operatingContext, hasActiveSprint, loading } = useMethodUiContext()
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])
const navItems = useMemo(() => {
if (!initiativeId) return WORK_NAV_ITEMS
if (loading && !operatingContext) return WORK_NAV_ITEMS
return resolveWorkNavItemsFromProfile({ operatingContext, hasActiveSprint })
}, [initiativeId, loading, operatingContext, hasActiveSprint])
return (
<ModeAreaNav
@ -63,33 +40,15 @@ export function WorkLayout() {
export function WorkIndexRedirect() {
const { initiativeId, hrefWithScope } = useProgramScope()
const [target, setTarget] = useState('/work/today')
const { workDefaultRoute, loading, operatingContext } = useMethodUiContext()
useEffect(() => {
if (!initiativeId) {
setTarget('/work/today')
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.workDefaultRoute))
})
.catch(() => {
if (!cancelled) setTarget(hrefWithScope('/work/today'))
})
return () => {
cancelled = true
}
}, [initiativeId, hrefWithScope])
if (!initiativeId) {
return <Navigate to="/work/today" replace />
}
return <Navigate to={target} replace />
if (loading && !operatingContext) {
return null
}
return <Navigate to={hrefWithScope(workDefaultRoute)} replace />
}

View File

@ -1,7 +1,6 @@
import { useEffect, useMemo } from 'react'
import { Link } from 'react-router-dom'
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx'
import { InitiativeActionsHub } from '../../components/InitiativeActionsHub.jsx'
import { NextActionWidget } from '../../widgets/NextActionWidget.jsx'
import { LoadingState } from '../../components/LoadingState.jsx'
@ -43,6 +42,7 @@ function WorkSprintInner() {
initiative,
operatingContext,
dataSlices,
uiFeatures,
steeringSnapshot,
steeringSnapshotLoading,
visibleActions,
@ -50,13 +50,20 @@ function WorkSprintInner() {
} = ops
const supportsWorkCycles = dataSlices.includes('work_cycles')
const isContinuousProduct = operatingContext?.method_key === 'continuous_product'
const isContinuousProductMode = uiFeatures.continuousProductWorkMode && supportsWorkCycles
const loadSteeringSnapshot = uiFeatures.steeringSnapshotOnWorkSprint
useEffect(() => {
if (initiativeId && !steeringSnapshot && !steeringSnapshotLoading) {
if (initiativeId && loadSteeringSnapshot && !steeringSnapshot && !steeringSnapshotLoading) {
reloadSlices(['steering_snapshot'])
}
}, [initiativeId, steeringSnapshot, steeringSnapshotLoading, reloadSlices])
}, [
initiativeId,
loadSteeringSnapshot,
steeringSnapshot,
steeringSnapshotLoading,
reloadSlices,
])
const defaultWorkCycleId = activeWorkCycle?.id || ''
@ -74,7 +81,7 @@ function WorkSprintInner() {
return <LoadingState message="Lade Sprint-Backlog …" />
}
const isProductArchetype = isContinuousProduct && supportsWorkCycles
const isProductArchetype = isContinuousProductMode
return (
<>
@ -86,7 +93,7 @@ function WorkSprintInner() {
<h2>Product-Ist (ohne aktiven Sprint)</h2>
<p className="section-lead muted">
Kein aktiver Sprint Kairo steuert nach{' '}
<strong>continuous_product</strong> (wirkungsvollster Schritt). Sprint-Planung
<strong>{operatingContext?.method_key || 'continuous_product'}</strong> (wirkungsvollster Schritt). Sprint-Planung
unter{' '}
<Link to={hrefWithScope('/plan/sprint')} className="link-inline">
Plan Sprint
@ -208,10 +215,8 @@ function WorkSprintInner() {
export function WorkSprintPage() {
return (
<RequireInitiativeScope lead="Sprint-Backlog ist vorhabenspezifisch — wähle ein Product-Vorhaben im Scope.">
<ScopedInitiativeProvider>
<WorkSprintInner />
</ScopedInitiativeProvider>
<RequireInitiativeScope lead="Sprint-Backlog ist vorhabenspezifisch — wähle ein passendes Vorhaben im Scope.">
<WorkSprintInner />
</RequireInitiativeScope>
)
}

View File

@ -0,0 +1,66 @@
/**
* AP2.3f Modus-Routen-Matching für Route Gating.
*/
import { MODE_ROUTE_GROUPS } from './viewRegistry.js'
/**
* @param {string} pathname
* @returns {{ layout: string, route: import('./viewRegistry.js').ModeRouteDefinition, fullPath: string } | null}
*/
export function matchModeRoute(pathname) {
const normalized = (pathname || '').replace(/\/$/, '') || pathname
for (const [layout, routes] of Object.entries(MODE_ROUTE_GROUPS)) {
for (const route of routes) {
const fullPath = route.routePath || `/${layout}/${route.path}`
if (normalized === fullPath || normalized.startsWith(`${fullPath}/`)) {
return { layout, route, fullPath }
}
}
}
return null
}
/**
* @param {string} pathname
* @param {{ operatingContext?: object | null, dataSlices?: string[] }} input
*/
export function isRouteAllowedForMode(pathname, input = {}) {
const match = matchModeRoute(pathname)
if (!match) return true
const { route } = match
const context = input.operatingContext
const dataSlices = input.dataSlices || context?.data_slices || []
const archetypeKey = context?.archetype_key || null
if (route.allowedArchetypes?.length && archetypeKey) {
if (!route.allowedArchetypes.includes(archetypeKey)) {
return false
}
}
if (route.requiredSlices?.length) {
return route.requiredSlices.every((slice) => dataSlices.includes(slice))
}
return true
}
/**
* @param {string} pathname
* @param {{ operatingContext?: object | null }} input
*/
export function resolveRedirectForModeRoute(pathname, input = {}) {
const match = matchModeRoute(pathname)
const layout = match?.layout || 'plan'
const profile = input.operatingContext?.ui_profile || {}
if (layout === 'work') {
return profile.workDefaultRoute || '/work/today'
}
if (layout === 'control') {
return profile.controlDefaultRoute || '/control/status'
}
return profile.planDefaultRoute || '/plan/profile'
}

View File

@ -0,0 +1,22 @@
import { Route } from 'react-router-dom'
import { OperatingRouteGate } from './OperatingRouteGate.jsx'
import { MODE_ROUTE_COMPONENTS, MODE_ROUTE_GROUPS } from './viewRegistry.js'
/**
* AP2.3f Modus-Subrouten aus Registry (kein App.jsx-Fork pro Archetyp).
* @param {'work'|'plan'|'control'} layout
*/
export function renderModeRoutes(layout) {
const routes = MODE_ROUTE_GROUPS[layout] || []
return routes.map((route) => {
const Component = MODE_ROUTE_COMPONENTS[route.componentKey]
const element = route.gated ? (
<OperatingRouteGate>
<Component />
</OperatingRouteGate>
) : (
<Component />
)
return <Route key={`${layout}-${route.path}`} path={route.path} element={element} />
})
}

View File

@ -1,5 +1,5 @@
/**
* AP2.3b Operating Profile Resolver (API-first, methodUiDefaults als Fallback).
* AP2.3b/f Operating Profile Resolver (API-first, methodUiDefaults nur als Fallback).
*/
import { PLAN_OUTLINE_NODES } from '../plan/planOutlineNodes.js'
import { WORK_NAV_ITEMS } from '../config/workNav.js'
@ -7,6 +7,10 @@ import {
resolveMethodUiDefaults,
resolveActiveProcessStepKey as resolveActiveProcessStepKeyFallback,
} from '../config/methodUiDefaults.js'
import {
isRouteAllowedForMode,
resolveRedirectForModeRoute,
} from './modeRouteRegistry.js'
/**
* @typedef {import('../config/methodUiDefaults.js').ProcessStep} ProcessStep
@ -16,7 +20,7 @@ import {
* archetype_key?: string | null,
* method_key?: string | null,
* method_profile_key?: string | null,
* ui_profile?: Partial<MethodUiDefaults> | null,
* ui_profile?: Partial<MethodUiDefaults & { uiFeatures?: Record<string, boolean>, workDefaultRouteWithoutActiveWorkCycle?: string }> | null,
* data_slices?: string[],
* method_capabilities?: object,
* }} OperatingContextResponse
@ -35,32 +39,49 @@ const EMPTY_PROFILE = {
controlDefaultRoute: '/control/status',
planOutlineKeys: null,
workNavKeys: null,
uiFeatures: {},
}
/** Methoden-Features ergänzen Archetyp-Profil (bis MethodDefinition ui_features trägt). */
const METHOD_UI_FEATURES = {
sequential_dependency: { criticalPathControl: true },
continuous_product: {
continuousProductWorkMode: true,
steeringSnapshotOnWorkSprint: true,
},
}
function mergeUiFeatures(profileFeatures, methodKey) {
const methodFeatures = METHOD_UI_FEATURES[methodKey] || {}
return { ...methodFeatures, ...(profileFeatures || {}) }
}
function resolveWorkDefaultRoute(profile, options = {}) {
const base = profile.workDefaultRoute || EMPTY_PROFILE.workDefaultRoute
if (!options.hasActiveSprint && profile.workDefaultRouteWithoutActiveWorkCycle) {
return profile.workDefaultRouteWithoutActiveWorkCycle
}
return base
}
/**
* @param {OperatingContextResponse | null | undefined} context
* @param {{ hasActiveSprint?: boolean }} [options]
* @returns {MethodUiDefaults & { dataSlices: string[] }}
* @returns {MethodUiDefaults & { dataSlices: string[], uiFeatures: Record<string, boolean> }}
*/
export function resolveOperatingProfile(context, options = {}) {
if (context?.ui_profile) {
const profile = context.ui_profile
let workDefaultRoute = profile.workDefaultRoute || EMPTY_PROFILE.workDefaultRoute
if (
context.archetype_key === 'initiative.product' &&
!options.hasActiveSprint
) {
workDefaultRoute = '/work/today'
}
return {
processSteps: profile.processSteps || [],
planDefaultRoute: profile.planDefaultRoute || EMPTY_PROFILE.planDefaultRoute,
workDefaultRoute,
workDefaultRoute: resolveWorkDefaultRoute(profile, options),
controlDefaultRoute:
profile.controlDefaultRoute || EMPTY_PROFILE.controlDefaultRoute,
planOutlineKeys: profile.planOutlineKeys ?? null,
workNavKeys: profile.workNavKeys ?? null,
dataSlices: context.data_slices || profile.dataSlices || [],
uiFeatures: mergeUiFeatures(profile.uiFeatures, context?.method_key),
}
}
@ -72,12 +93,13 @@ export function resolveOperatingProfile(context, options = {}) {
return {
...fallback,
dataSlices: context?.data_slices || [],
uiFeatures: mergeUiFeatures({}, context?.method_key),
}
}
/**
* @param {ResolveOperatingProfileInput} input
* @returns {MethodUiDefaults & { dataSlices: string[] }}
* @returns {MethodUiDefaults & { dataSlices: string[], uiFeatures: Record<string, boolean> }}
*/
export function resolveOperatingProfileFromInput(input = {}) {
if (input.operatingContext) {
@ -90,7 +112,7 @@ export function resolveOperatingProfileFromInput(input = {}) {
methodKey: input.methodKey || null,
hasActiveSprint: input.hasActiveSprint,
})
return { ...fallback, dataSlices: [] }
return { ...fallback, dataSlices: [], uiFeatures: {} }
}
/**
@ -132,23 +154,10 @@ export function resolveActiveProcessStepKey(pathname, steps) {
*/
export function isRouteAllowedForProfile(routePath, input = {}) {
const profile = resolveOperatingProfileFromInput(input)
const normalized = (routePath || '').replace(/\/$/, '')
if (normalized.startsWith('/plan/sprint') || normalized.startsWith('/work/sprint')) {
return profile.dataSlices.includes('work_cycles')
}
if (normalized.startsWith('/plan/structure')) {
return profile.dataSlices.includes('projects')
}
if (normalized.startsWith('/plan/inbox')) {
return profile.dataSlices.includes('backlog')
}
if (normalized.startsWith('/control/journey')) {
const steps = profile.processSteps || []
return steps.some((s) => s.key === 'journey')
}
return true
return isRouteAllowedForMode(routePath, {
operatingContext: input.operatingContext,
dataSlices: profile.dataSlices,
})
}
/**
@ -157,14 +166,12 @@ export function isRouteAllowedForProfile(routePath, input = {}) {
* @returns {string}
*/
export function resolveRedirectForDisallowedRoute(routePath, input = {}) {
if (input.operatingContext) {
return resolveRedirectForModeRoute(routePath, input)
}
const profile = resolveOperatingProfileFromInput(input)
const normalized = (routePath || '').replace(/\/$/, '')
if (normalized.startsWith('/work/')) {
return profile.workDefaultRoute
}
if (normalized.startsWith('/control/')) {
return profile.controlDefaultRoute
}
if (normalized.startsWith('/work/')) return profile.workDefaultRoute
if (normalized.startsWith('/control/')) return profile.controlDefaultRoute
return profile.planDefaultRoute
}

View File

@ -17,6 +17,7 @@ const PRODUCT_CONTEXT = {
],
planDefaultRoute: '/plan/inbox',
workDefaultRoute: '/work/sprint',
workDefaultRouteWithoutActiveWorkCycle: '/work/today',
controlDefaultRoute: '/control/status',
planOutlineKeys: ['profile', 'inbox', 'sprint', 'gates'],
workNavKeys: ['sprint', 'today', 'mine'],
@ -73,4 +74,25 @@ describe('resolveOperatingProfile', () => {
const input = { operatingContext: PRODUCT_CONTEXT }
expect(isRouteAllowedForProfile('/plan/sprint', input)).toBe(true)
})
it('liefert uiFeatures aus Archetyp-Profil', () => {
const profile = resolveOperatingProfile({
...PRODUCT_CONTEXT,
ui_profile: {
...PRODUCT_CONTEXT.ui_profile,
uiFeatures: { continuousProductWorkMode: true },
},
})
expect(profile.uiFeatures.continuousProductWorkMode).toBe(true)
})
it('sequential_dependency ergänzt criticalPathControl', () => {
const profile = resolveOperatingProfile({
archetype_key: 'initiative.generic',
method_key: 'sequential_dependency',
ui_profile: { uiFeatures: {} },
data_slices: ['actions'],
})
expect(profile.uiFeatures.criticalPathControl).toBe(true)
})
})

View File

@ -144,36 +144,54 @@ export const MODE_ROUTE_COMPONENTS = {
controlJourney: ControlJourneyPage,
}
/** Modus-Routen mit Slice-Gating (AP2.3d). */
/** Modus-Routen mit Slice-Gating (AP2.3d/f). */
/** @typedef {{ path: string, componentKey: keyof typeof MODE_ROUTE_COMPONENTS, requiredSlices?: string[], allowedArchetypes?: string[], routePath?: string, gated?: boolean }} ModeRouteDefinition */
export const MODE_ROUTE_GROUPS = {
work: [
{ path: 'today', componentKey: 'workToday', requiredSlices: ['actions'], gated: true },
{ path: 'mine', componentKey: 'workMine', requiredSlices: ['actions'], gated: true },
{
path: 'sprint',
componentKey: 'workSprint',
requiredSlices: ['work_cycles'],
routePath: '/work/sprint',
gated: true,
},
],
plan: [
{ path: 'portfolio', componentKey: 'planPortfolio', gated: false },
{ path: 'profile', componentKey: 'planProfile', gated: false },
{ path: 'structure', componentKey: 'planStructure', requiredSlices: ['projects'], gated: true },
{ path: 'gates', componentKey: 'planGates', requiredSlices: ['roadmap'], gated: true },
{ path: 'inbox', componentKey: 'planInbox', requiredSlices: ['backlog'], gated: true },
{
path: 'sprint',
componentKey: 'planSprint',
requiredSlices: ['work_cycles'],
routePath: '/plan/sprint',
gated: true,
},
{ path: 'work', componentKey: 'planWork', requiredSlices: ['actions'], gated: false },
],
control: [
{ path: 'status', componentKey: 'controlStatus', gated: false },
{ path: 'plan-ist', componentKey: 'controlPlanIst', requiredSlices: ['roadmap'], gated: true },
{
path: 'journey',
componentKey: 'controlJourney',
requiredSlices: ['roadmap'],
routePath: '/control/journey',
gated: true,
},
],
}
/** @deprecated Use MODE_ROUTE_GROUPS */
export const MODE_ROUTES = [
{ path: 'today', componentKey: 'workToday', requiredSlices: ['actions'] },
{ path: 'mine', componentKey: 'workMine', requiredSlices: ['actions'] },
{
path: 'sprint',
componentKey: 'workSprint',
requiredSlices: ['work_cycles'],
routePath: '/work/sprint',
},
{ path: 'portfolio', componentKey: 'planPortfolio', requiredSlices: [] },
{ path: 'profile', componentKey: 'planProfile', requiredSlices: [] },
{ path: 'structure', componentKey: 'planStructure', requiredSlices: ['projects'] },
{ path: 'gates', componentKey: 'planGates', requiredSlices: ['roadmap'] },
{ path: 'inbox', componentKey: 'planInbox', requiredSlices: ['backlog'] },
{
path: 'sprint',
componentKey: 'planSprint',
requiredSlices: ['work_cycles'],
routePath: '/plan/sprint',
},
{ path: 'work', componentKey: 'planWork', requiredSlices: ['actions'] },
{ path: 'status', componentKey: 'controlStatus', requiredSlices: [] },
{ path: 'plan-ist', componentKey: 'controlPlanIst', requiredSlices: ['roadmap'] },
{
path: 'journey',
componentKey: 'controlJourney',
requiredSlices: ['roadmap'],
routePath: '/control/journey',
},
...MODE_ROUTE_GROUPS.work,
...MODE_ROUTE_GROUPS.plan,
...MODE_ROUTE_GROUPS.control,
]
export {