AP2.3d: Route Gating via OperatingRouteGate und Slice-basierte Seitenlogik.
Some checks failed
Test Suite / pytest-backend (push) Waiting to run
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) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lars 2026-07-25 10:27:20 +02:00
parent 1b55d8558c
commit 11e36e674e
4 changed files with 80 additions and 7 deletions

View File

@ -7,6 +7,7 @@ export function InitiativeInboxPage() {
initiative,
initiativeId,
backlogItems,
dataSlices,
roadmapItems,
featureParentActions,
workCycles,
@ -31,8 +32,6 @@ export function InitiativeInboxPage() {
return <LoadingState message="Lade Eingang …" />
}
const isProductArchetype = initiative?.archetype_key === 'initiative.product'
return (
<>
{error && <p className="error">{error}</p>}
@ -43,7 +42,7 @@ export function InitiativeInboxPage() {
featureActions={featureParentActions}
workCycles={workCycles}
activeWorkCycle={activeWorkCycle}
sprintPlanningEnabled={isProductArchetype}
sprintPlanningEnabled={dataSlices.includes('work_cycles')}
canManage={capabilities.has('kairo.backlog.manage')}
onCreate={handleCreateBacklog}
onUpdate={handleUpdateBacklog}

View File

@ -1,4 +1,4 @@
import { useMemo } from 'react'
import { useEffect, useMemo } from 'react'
import { Link } from 'react-router-dom'
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx'
@ -41,11 +41,23 @@ function WorkSprintInner() {
handleMoveActionToSprint,
handleUnplanAction,
initiative,
operatingContext,
dataSlices,
steeringSnapshot,
steeringSnapshotLoading,
visibleActions,
reloadSlices,
} = ops
const supportsWorkCycles = dataSlices.includes('work_cycles')
const isContinuousProduct = operatingContext?.method_key === 'continuous_product'
useEffect(() => {
if (initiativeId && !steeringSnapshot && !steeringSnapshotLoading) {
reloadSlices(['steering_snapshot'])
}
}, [initiativeId, steeringSnapshot, steeringSnapshotLoading, reloadSlices])
const defaultWorkCycleId = activeWorkCycle?.id || ''
const createAction = useMemo(
@ -62,7 +74,7 @@ function WorkSprintInner() {
return <LoadingState message="Lade Sprint-Backlog …" />
}
const isProductArchetype = initiative?.archetype_key === 'initiative.product'
const isProductArchetype = isContinuousProduct && supportsWorkCycles
return (
<>
@ -187,8 +199,8 @@ function WorkSprintInner() {
</>
)}
{!activeWorkCycle && !isProductArchetype && (
<EmptyState message="Sprint-Ausführung ist für Product-Vorhaben — wähle ein passendes Vorhaben im Scope." />
{!activeWorkCycle && !supportsWorkCycles && (
<EmptyState message="Sprint-Ausführung ist für diesen Vorhabentyp nicht verfügbar." />
)}
</>
)

View File

@ -0,0 +1,30 @@
import { Navigate, useLocation } from 'react-router-dom'
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
import { useMethodUiContext } from '../hooks/useMethodUiContext.js'
import {
isRouteAllowedForProfile,
resolveRedirectForDisallowedRoute,
} from './resolveOperatingProfile.js'
/**
* AP2.3d Redirect wenn Route für Archetyp/Slice unpassend.
*/
export function OperatingRouteGate({ children }) {
const location = useLocation()
const { hrefWithScope, initiativeId } = useProgramScope()
const { operatingContext, loading } = useMethodUiContext()
if (!initiativeId || loading) {
return children
}
const input = { operatingContext }
const path = location.pathname
if (isRouteAllowedForProfile(path, input)) {
return children
}
const redirect = resolveRedirectForDisallowedRoute(path, input)
return <Navigate to={hrefWithScope(redirect)} replace />
}

View File

@ -144,6 +144,38 @@ export const MODE_ROUTE_COMPONENTS = {
controlJourney: ControlJourneyPage,
}
/** Modus-Routen mit Slice-Gating (AP2.3d). */
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',
},
]
export {
WorkLayout,
WorkIndexRedirect,