diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 74cb635..82da70b 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,6 +1,7 @@
import { Navigate, Route, Routes } from 'react-router-dom'
import { SessionProvider, useSession } from './context/SessionContext.jsx'
import { ProgramScopeProvider } from './context/ProgramScopeContext.jsx'
+import { ScopedOperationsBridge } from './layout/ScopedOperationsBridge.jsx'
import { AppLayout } from './layout/AppLayout.jsx'
import { AuthPanel } from './pages/AuthPanel.jsx'
import { ActionDetailPage } from './pages/ActionDetailPage.jsx'
@@ -16,6 +17,7 @@ import {
ControlIndexRedirect,
MODE_ROUTE_COMPONENTS,
} from './registry/viewRegistry.js'
+import { OperatingRouteGate } from './registry/OperatingRouteGate.jsx'
import {
WorkspaceRedirect,
MyActionsRedirect,
@@ -54,35 +56,99 @@ function AppRoutes() {
return (
-
- }>
+
+
+ }>
{topLevelViews.map((view) => (
} />
))}
}>
} />
- } />
- } />
- } />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
}>
} />
} />
} />
- } />
- } />
- } />
- } />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
} />
}>
} />
} />
- } />
- } />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
} />
@@ -109,6 +175,7 @@ function AppRoutes() {
} />
+
)
}
diff --git a/frontend/src/components/PlanOutlineNav.jsx b/frontend/src/components/PlanOutlineNav.jsx
index afbcf93..6203dd7 100644
--- a/frontend/src/components/PlanOutlineNav.jsx
+++ b/frontend/src/components/PlanOutlineNav.jsx
@@ -1,10 +1,9 @@
import { NavLink, useLocation } from 'react-router-dom'
import { useEffect, useMemo, useState } from 'react'
-import { listInitiativeProjects } from '../api/projects.js'
-import { listInitiativeActions } from '../api/initiatives.js'
-import { listInitiativeBacklog } from '../api/backlog.js'
import { getInitiativeExecutionGraphState } from '../api/executionPlan.js'
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
+import { useOptionalInitiativeOperations } from '../context/InitiativeOperationsContext.jsx'
+import { useMethodUiContext } from '../hooks/useMethodUiContext.js'
import { buildProjectsByParent } from '../utils/projectTree.js'
import { projectPath } from '../utils/routes.js'
import {
@@ -12,9 +11,7 @@ import {
resolvePlanOutlineProjectId,
resolvePlanOutlineActionId,
} from '../plan/planOutlineNodes.js'
-import { resolvePlanOutlineNodes } from '../config/methodUiDefaults.js'
-import { getInitiative } from '../api/initiatives.js'
-import { getActiveWorkCycle } from '../api/workCycles.js'
+import { resolvePlanOutlineNodesFromProfile } from '../registry/resolveOperatingProfile.js'
import { PlanOutlineWorkTree } from './PlanOutlineWorkTree.jsx'
function PlanOutlineProjectTree({ projects, parentId, activeProjectId, depth = 0 }) {
@@ -55,15 +52,12 @@ function PlanOutlineProjectTree({ projects, parentId, activeProjectId, depth = 0
export function PlanOutlineNav() {
const location = useLocation()
const { initiativeId, initiativeTitle, hrefWithScope } = useProgramScope()
+ const { operatingContext, hasActiveSprint } = useMethodUiContext()
+ const ops = useOptionalInitiativeOperations()
const activeKey = resolvePlanOutlineActiveKey(location.pathname)
const activeProjectId = resolvePlanOutlineProjectId(location.pathname)
const activeActionId = resolvePlanOutlineActionId(location.pathname)
- const [counts, setCounts] = useState({ inbox: null, work: null })
- const [projects, setProjects] = useState([])
- const [openActions, setOpenActions] = useState([])
const [executionGraph, setExecutionGraph] = useState(null)
- const [archetypeKey, setArchetypeKey] = useState(null)
- const [hasActiveSprint, setHasActiveSprint] = useState(false)
const [structureOpen, setStructureOpen] = useState(
activeKey === 'structure' || Boolean(activeProjectId),
)
@@ -71,6 +65,29 @@ export function PlanOutlineNav() {
activeKey === 'work' || Boolean(activeActionId),
)
+ const contextReady = Boolean(initiativeId && ops?.initiativeId === initiativeId)
+ const projects = contextReady ? ops.projects : []
+ const backlogItems = contextReady ? ops.backlogItems : []
+ const actions = contextReady ? ops.actions : []
+
+ const openActions = useMemo(
+ () =>
+ actions.filter(
+ (action) => action.status !== 'done' && action.status !== 'discarded',
+ ),
+ [actions],
+ )
+
+ const counts = useMemo(
+ () => ({
+ inbox: contextReady
+ ? backlogItems.filter((item) => item.status !== 'converted').length
+ : null,
+ work: contextReady ? openActions.length : null,
+ }),
+ [contextReady, backlogItems, openActions],
+ )
+
useEffect(() => {
setStructureOpen(activeKey === 'structure' || Boolean(activeProjectId))
}, [activeKey, activeProjectId])
@@ -79,85 +96,31 @@ export function PlanOutlineNav() {
setWorkOpen(activeKey === 'work' || Boolean(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,
+ resolvePlanOutlineNodesFromProfile({
+ operatingContext,
hasActiveSprint,
}),
- [archetypeKey, hasActiveSprint],
+ [operatingContext, hasActiveSprint],
)
useEffect(() => {
if (!initiativeId) {
- setProjects([])
+ setExecutionGraph(null)
return undefined
}
let cancelled = false
- listInitiativeProjects(initiativeId)
- .then((items) => {
- if (!cancelled) setProjects(Array.isArray(items) ? items : [])
+ getInitiativeExecutionGraphState(initiativeId)
+ .then((graphState) => {
+ if (!cancelled) setExecutionGraph(graphState)
})
.catch(() => {
- if (!cancelled) setProjects([])
+ if (!cancelled) setExecutionGraph(null)
})
return () => {
cancelled = true
}
- }, [initiativeId])
-
- useEffect(() => {
- if (!initiativeId) {
- setCounts({ inbox: null, work: null })
- return undefined
- }
- let cancelled = false
- Promise.all([
- listInitiativeBacklog(initiativeId).catch(() => []),
- listInitiativeActions(initiativeId).catch(() => []),
- getInitiativeExecutionGraphState(initiativeId).catch(() => null),
- ]).then(([backlog, actions, graphState]) => {
- if (cancelled) return
- const open = actions.filter(
- (action) => action.status !== 'done' && action.status !== 'discarded',
- )
- setOpenActions(open)
- setExecutionGraph(graphState)
- setCounts({
- inbox: backlog.filter((item) => item.status !== 'converted').length,
- work: open.length,
- })
- })
- return () => {
- cancelled = true
- }
}, [initiativeId, location.pathname])
function nodeLabel(node) {
diff --git a/frontend/src/context/InitiativeOperationsContext.jsx b/frontend/src/context/InitiativeOperationsContext.jsx
index 1ff54da..ecb754d 100644
--- a/frontend/src/context/InitiativeOperationsContext.jsx
+++ b/frontend/src/context/InitiativeOperationsContext.jsx
@@ -10,6 +10,7 @@ import { useParams } from 'react-router-dom'
import {
getInitiative,
getInitiativeSteeringSnapshot,
+ getInitiativeOperatingContext,
listInitiativeActions,
createInitiativeAction,
updateInitiative,
@@ -86,6 +87,10 @@ import { useOptionalProgramScope } from './ProgramScopeContext.jsx'
import { collectProjectSubtreeIds } from '../utils/projectTree.js'
import { filterActionsForWorkCycle } from '../utils/workCycleActions.js'
import { listFeatureParentActions } from '../utils/actionReferences.js'
+import {
+ applySliceResults,
+ loadOperatingSlices,
+} from '../registry/operatingSliceLoaders.js'
const InitiativeOperationsContext = createContext(null)
@@ -114,6 +119,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
const [steeringSnapshotLoading, setSteeringSnapshotLoading] = useState(false)
const [steeringSnapshotError, setSteeringSnapshotError] = useState(null)
const [steeringMethods, setSteeringMethods] = useState([])
+ const [operatingContext, setOperatingContext] = useState(null)
const [methodBusy, setMethodBusy] = useState(false)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
@@ -123,62 +129,74 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
const [hideDone, setHideDone] = useState(true)
const [selectedWorkCycleId, setSelectedWorkCycleId] = useState('')
- const load = useCallback(async ({ silent = false } = {}) => {
+ const sliceSetters = useMemo(
+ () => ({
+ setInitiative,
+ setActions,
+ setBlockers,
+ setBacklogItems,
+ setRoadmapItems,
+ setProjects,
+ setEvidenceItems,
+ setDecisions,
+ setReviews,
+ setRecurringItems,
+ setWorkCycles,
+ setActiveWorkCycle,
+ setSteeringMethods,
+ setSteeringSnapshot,
+ }),
+ [],
+ )
+
+ const reloadSlices = useCallback(
+ async (sliceKeys) => {
+ if (!id || !sliceKeys?.length) return
+ const results = await loadOperatingSlices(id, sliceKeys)
+ applySliceResults(results, sliceSetters)
+ },
+ [id, sliceSetters],
+ )
+
+ const load = useCallback(async ({ silent = false, sliceKeys = null } = {}) => {
if (sessionLoading || !id) return
if (!silent) setLoading(true)
setError(null)
try {
- const [initData, actionData] = await Promise.all([
- getInitiative(id),
- listInitiativeActions(id),
- ])
- setInitiative(initData)
- setActions(actionData)
-
- const loads = []
const canReadOm =
capabilities.has('kairo.initiative.read') ||
capabilities.has('kairo.blocker.read')
- if (canReadOm) {
- loads.push(listInitiativeBlockers(id).then(setBlockers).catch(() => setBlockers([])))
- loads.push(listInitiativeBacklog(id).then(setBacklogItems).catch(() => setBacklogItems([])))
- loads.push(listInitiativeRoadmapItems(id).then(setRoadmapItems).catch(() => setRoadmapItems([])))
- loads.push(listInitiativeProjects(id).then(setProjects).catch(() => setProjects([])))
- loads.push(listInitiativeEvidence(id).then(setEvidenceItems).catch(() => setEvidenceItems([])))
- loads.push(listInitiativeDecisions(id).then(setDecisions).catch(() => setDecisions([])))
- loads.push(listInitiativeReviews(id).then(setReviews).catch(() => setReviews([])))
- loads.push(listInitiativeRecurring(id).then(setRecurringItems).catch(() => setRecurringItems([])))
- loads.push(listInitiativeWorkCycles(id).then(setWorkCycles).catch(() => setWorkCycles([])))
- loads.push(
- getActiveWorkCycle(id)
- .then((item) => setActiveWorkCycle(item || null))
- .catch(() => setActiveWorkCycle(null))
- )
- loads.push(
- listSteeringMethods().then(setSteeringMethods).catch(() => setSteeringMethods([]))
- )
- loads.push(
- (async () => {
- setSteeringSnapshotLoading(true)
- setSteeringSnapshotError(null)
- try {
- setSteeringSnapshot(await getInitiativeSteeringSnapshot(id))
- } catch (err) {
- setSteeringSnapshot(null)
- setSteeringSnapshotError(err.message || 'Snapshot fehlgeschlagen')
- } finally {
- setSteeringSnapshotLoading(false)
- }
- })()
- )
+
+ let opContext = null
+ if (!sliceKeys) {
+ opContext = await getInitiativeOperatingContext(id).catch(() => null)
+ setOperatingContext(opContext)
+ }
+
+ const omSlices = canReadOm ? opContext?.data_slices || [] : []
+ const keysToLoad = sliceKeys || ['initiative', ...omSlices]
+
+ if (keysToLoad.includes('steering_snapshot')) {
+ setSteeringSnapshotLoading(true)
+ setSteeringSnapshotError(null)
+ }
+
+ const results = await loadOperatingSlices(id, keysToLoad)
+ applySliceResults(results, sliceSetters)
+
+ if (keysToLoad.includes('steering_snapshot') && results.steering_snapshot == null) {
+ setSteeringSnapshotError('Snapshot fehlgeschlagen')
+ }
+ if (keysToLoad.includes('steering_snapshot')) {
+ setSteeringSnapshotLoading(false)
}
- await Promise.all(loads)
} catch (err) {
setError(err.message)
+ setSteeringSnapshotLoading(false)
} finally {
setLoading(false)
}
- }, [id, capabilities, sessionLoading])
+ }, [id, capabilities, sessionLoading, sliceSetters])
useEffect(() => {
load()
@@ -380,6 +398,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
try {
const updated = await updateAction(action.id, { work_cycle_id: targetCycleId })
setActions((prev) => prev.map((a) => (a.id === action.id ? updated : a)))
+ await reloadSlices(['work_cycles'])
} catch (err) {
setError(err.message)
} finally {
@@ -975,6 +994,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
steeringSnapshotLoading,
steeringSnapshotError,
steeringMethods,
+ operatingContext,
+ dataSlices: operatingContext?.data_slices || [],
methodBusy,
loading,
error,
@@ -993,6 +1014,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
actorsUsedFallback: actorsState.usedFallback,
reloadActors: actorsState.reload,
reload: load,
+ reloadSlices,
handleMethodChange,
handleMethodProfileChange,
handleCreateAction,
@@ -1053,8 +1075,12 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
)
}
+export function useOptionalInitiativeOperations() {
+ return useContext(InitiativeOperationsContext)
+}
+
export function useInitiativeOperations() {
- const ctx = useContext(InitiativeOperationsContext)
+ const ctx = useOptionalInitiativeOperations()
if (!ctx) {
throw new Error('useInitiativeOperations must be used within InitiativeOperationsProvider')
}
diff --git a/frontend/src/layout/ScopedInitiativeProvider.jsx b/frontend/src/layout/ScopedInitiativeProvider.jsx
index b4dca62..355f515 100644
--- a/frontend/src/layout/ScopedInitiativeProvider.jsx
+++ b/frontend/src/layout/ScopedInitiativeProvider.jsx
@@ -1,13 +1,6 @@
-import { InitiativeOperationsProvider } from '../context/InitiativeOperationsContext.jsx'
-import { useProgramScope } from '../context/ProgramScopeContext.jsx'
-
-export function ScopedInitiativeProvider({ initiativeId: initiativeIdProp, children }) {
- const { initiativeId: scopeInitiativeId } = useProgramScope()
- const initiativeId = initiativeIdProp || scopeInitiativeId
-
- return (
-
- {children}
-
- )
+/**
+ * AP2.3c — Scope-Gate ohne eigenen Data-Provider (Provider in ProgramScopeContext).
+ */
+export function ScopedInitiativeProvider({ children }) {
+ return children
}
diff --git a/frontend/src/layout/ScopedOperationsBridge.jsx b/frontend/src/layout/ScopedOperationsBridge.jsx
new file mode 100644
index 0000000..299427b
--- /dev/null
+++ b/frontend/src/layout/ScopedOperationsBridge.jsx
@@ -0,0 +1,12 @@
+import { InitiativeOperationsProvider } from '../context/InitiativeOperationsContext.jsx'
+import { useProgramScope } from '../context/ProgramScopeContext.jsx'
+
+/** AP2.3c — ein Provider pro Vorhaben-Scope (unter ProgramScope). */
+export function ScopedOperationsBridge({ children }) {
+ const { initiativeId } = useProgramScope()
+ return (
+
+ {children}
+
+ )
+}
diff --git a/frontend/src/registry/operatingSliceLoaders.js b/frontend/src/registry/operatingSliceLoaders.js
new file mode 100644
index 0000000..4f01aad
--- /dev/null
+++ b/frontend/src/registry/operatingSliceLoaders.js
@@ -0,0 +1,107 @@
+/**
+ * AP2.3c — Data Slice Registry (lazy OM loads pro Operating Context).
+ */
+import {
+ getInitiative,
+ getInitiativeSteeringSnapshot,
+ listInitiativeActions,
+} from '../api/initiatives.js'
+import { listInitiativeBlockers } from '../api/blockers.js'
+import { listInitiativeBacklog } from '../api/backlog.js'
+import { listInitiativeRoadmapItems } from '../api/roadmap.js'
+import { listInitiativeProjects } from '../api/projects.js'
+import { listInitiativeEvidence } from '../api/evidence.js'
+import { listInitiativeDecisions } from '../api/decisions.js'
+import { listInitiativeReviews } from '../api/reviews.js'
+import { listInitiativeRecurring } from '../api/recurring.js'
+import {
+ listInitiativeWorkCycles,
+ getActiveWorkCycle,
+} from '../api/workCycles.js'
+import { listSteeringMethods } from '../api/steering.js'
+
+/** @type {Record Promise>} */
+export const SLICE_LOADERS = {
+ initiative: (initiativeId) => getInitiative(initiativeId),
+ actions: (initiativeId) => listInitiativeActions(initiativeId),
+ blockers: (initiativeId) => listInitiativeBlockers(initiativeId),
+ backlog: (initiativeId) => listInitiativeBacklog(initiativeId),
+ roadmap: (initiativeId) => listInitiativeRoadmapItems(initiativeId),
+ projects: (initiativeId) => listInitiativeProjects(initiativeId),
+ evidence: (initiativeId) => listInitiativeEvidence(initiativeId),
+ decisions: (initiativeId) => listInitiativeDecisions(initiativeId),
+ reviews: (initiativeId) => listInitiativeReviews(initiativeId),
+ recurring: (initiativeId) => listInitiativeRecurring(initiativeId),
+ work_cycles: async (initiativeId) => ({
+ workCycles: await listInitiativeWorkCycles(initiativeId),
+ activeWorkCycle: await getActiveWorkCycle(initiativeId).catch(() => null),
+ }),
+ steering_methods: () => listSteeringMethods(),
+ steering_snapshot: (initiativeId) => getInitiativeSteeringSnapshot(initiativeId),
+}
+
+const CORE_SLICES = ['actions']
+
+/**
+ * @param {string} initiativeId
+ * @param {string[]} sliceKeys
+ * @returns {Promise>}
+ */
+export async function loadOperatingSlices(initiativeId, sliceKeys) {
+ const keys = new Set(['initiative', 'actions', ...(sliceKeys || [])])
+ const entries = await Promise.all(
+ [...keys].map(async (key) => {
+ const loader = SLICE_LOADERS[key]
+ if (!loader) return [key, null]
+ try {
+ const value = await loader(initiativeId)
+ return [key, value]
+ } catch {
+ return [key, null]
+ }
+ }),
+ )
+ return Object.fromEntries(entries)
+}
+
+/**
+ * Wendet Slice-Ergebnisse auf InitiativeOperations-State-Setter an.
+ * @param {Record} slices
+ * @param {object} setters
+ */
+export function applySliceResults(slices, setters) {
+ if (slices.initiative != null) setters.setInitiative(slices.initiative)
+ if (Array.isArray(slices.actions)) setters.setActions(slices.actions)
+ if (Array.isArray(slices.blockers)) setters.setBlockers(slices.blockers)
+ if (Array.isArray(slices.backlog)) setters.setBacklogItems(slices.backlog)
+ if (Array.isArray(slices.roadmap)) setters.setRoadmapItems(slices.roadmap)
+ if (Array.isArray(slices.projects)) setters.setProjects(slices.projects)
+ if (Array.isArray(slices.evidence)) setters.setEvidenceItems(slices.evidence)
+ if (Array.isArray(slices.decisions)) setters.setDecisions(slices.decisions)
+ if (Array.isArray(slices.reviews)) setters.setReviews(slices.reviews)
+ if (Array.isArray(slices.recurring)) setters.setRecurringItems(slices.recurring)
+ if (Array.isArray(slices.steering_methods)) setters.setSteeringMethods(slices.steering_methods)
+
+ if (slices.work_cycles && typeof slices.work_cycles === 'object') {
+ const wc = /** @type {{ workCycles?: unknown[], activeWorkCycle?: unknown }} */ (
+ slices.work_cycles
+ )
+ if (Array.isArray(wc.workCycles)) setters.setWorkCycles(wc.workCycles)
+ setters.setActiveWorkCycle(wc.activeWorkCycle || null)
+ }
+
+ if (slices.steering_snapshot != null) {
+ setters.setSteeringSnapshot(slices.steering_snapshot)
+ }
+}
+
+/**
+ * @param {string} sliceKey
+ * @param {string} initiativeId
+ * @returns {Promise}
+ */
+export async function reloadOperatingSlice(sliceKey, initiativeId) {
+ const loader = SLICE_LOADERS[sliceKey]
+ if (!loader) return null
+ return loader(initiativeId)
+}