AP2.3c: Slice-bewusstes Laden und hoisted InitiativeOperationsProvider.
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:26:56 +02:00
parent 90934453e6
commit 1b55d8558c
6 changed files with 308 additions and 140 deletions

View File

@ -1,6 +1,7 @@
import { Navigate, Route, Routes } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { SessionProvider, useSession } from './context/SessionContext.jsx' import { SessionProvider, useSession } from './context/SessionContext.jsx'
import { ProgramScopeProvider } from './context/ProgramScopeContext.jsx' import { ProgramScopeProvider } from './context/ProgramScopeContext.jsx'
import { ScopedOperationsBridge } from './layout/ScopedOperationsBridge.jsx'
import { AppLayout } from './layout/AppLayout.jsx' import { AppLayout } from './layout/AppLayout.jsx'
import { AuthPanel } from './pages/AuthPanel.jsx' import { AuthPanel } from './pages/AuthPanel.jsx'
import { ActionDetailPage } from './pages/ActionDetailPage.jsx' import { ActionDetailPage } from './pages/ActionDetailPage.jsx'
@ -16,6 +17,7 @@ import {
ControlIndexRedirect, ControlIndexRedirect,
MODE_ROUTE_COMPONENTS, MODE_ROUTE_COMPONENTS,
} from './registry/viewRegistry.js' } from './registry/viewRegistry.js'
import { OperatingRouteGate } from './registry/OperatingRouteGate.jsx'
import { import {
WorkspaceRedirect, WorkspaceRedirect,
MyActionsRedirect, MyActionsRedirect,
@ -54,35 +56,99 @@ function AppRoutes() {
return ( return (
<ProgramScopeProvider> <ProgramScopeProvider>
<Routes> <ScopedOperationsBridge>
<Route element={<AppLayout />}> <Routes>
<Route element={<AppLayout />}>
{topLevelViews.map((view) => ( {topLevelViews.map((view) => (
<Route key={view.key} path={view.path} element={<view.component />} /> <Route key={view.key} path={view.path} element={<view.component />} />
))} ))}
<Route path="/work" element={<WorkLayout />}> <Route path="/work" element={<WorkLayout />}>
<Route index element={<WorkIndexRedirect />} /> <Route index element={<WorkIndexRedirect />} />
<Route path="today" element={<MODE_ROUTE_COMPONENTS.workToday />} /> <Route
<Route path="sprint" element={<MODE_ROUTE_COMPONENTS.workSprint />} /> path="today"
<Route path="mine" element={<MODE_ROUTE_COMPONENTS.workMine />} /> 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>
}
/>
</Route> </Route>
<Route path="/plan" element={<PlanLayout />}> <Route path="/plan" element={<PlanLayout />}>
<Route index element={<PlanIndexRedirect />} /> <Route index element={<PlanIndexRedirect />} />
<Route path="portfolio" element={<MODE_ROUTE_COMPONENTS.planPortfolio />} /> <Route path="portfolio" element={<MODE_ROUTE_COMPONENTS.planPortfolio />} />
<Route path="profile" element={<MODE_ROUTE_COMPONENTS.planProfile />} /> <Route path="profile" element={<MODE_ROUTE_COMPONENTS.planProfile />} />
<Route path="structure" element={<MODE_ROUTE_COMPONENTS.planStructure />} /> <Route
<Route path="gates" element={<MODE_ROUTE_COMPONENTS.planGates />} /> path="structure"
<Route path="inbox" element={<MODE_ROUTE_COMPONENTS.planInbox />} /> element={
<Route path="sprint" element={<MODE_ROUTE_COMPONENTS.planSprint />} /> <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 />} /> <Route path="work" element={<MODE_ROUTE_COMPONENTS.planWork />} />
</Route> </Route>
<Route path="/control" element={<ControlLayout />}> <Route path="/control" element={<ControlLayout />}>
<Route index element={<ControlIndexRedirect />} /> <Route index element={<ControlIndexRedirect />} />
<Route path="status" element={<MODE_ROUTE_COMPONENTS.controlStatus />} /> <Route path="status" element={<MODE_ROUTE_COMPONENTS.controlStatus />} />
<Route path="plan-ist" element={<MODE_ROUTE_COMPONENTS.controlPlanIst />} /> <Route
<Route path="journey" element={<MODE_ROUTE_COMPONENTS.controlJourney />} /> path="plan-ist"
element={
<OperatingRouteGate>
<MODE_ROUTE_COMPONENTS.controlPlanIst />
</OperatingRouteGate>
}
/>
<Route
path="journey"
element={
<OperatingRouteGate>
<MODE_ROUTE_COMPONENTS.controlJourney />
</OperatingRouteGate>
}
/>
</Route> </Route>
<Route path="/projects/:projectId" element={<ProjectObjectPage />} /> <Route path="/projects/:projectId" element={<ProjectObjectPage />} />
@ -109,6 +175,7 @@ function AppRoutes() {
<Route path="*" element={<Navigate to="/cockpit" replace />} /> <Route path="*" element={<Navigate to="/cockpit" replace />} />
</Route> </Route>
</Routes> </Routes>
</ScopedOperationsBridge>
</ProgramScopeProvider> </ProgramScopeProvider>
) )
} }

View File

@ -1,10 +1,9 @@
import { NavLink, useLocation } from 'react-router-dom' import { NavLink, useLocation } from 'react-router-dom'
import { useEffect, useMemo, useState } from 'react' 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 { getInitiativeExecutionGraphState } from '../api/executionPlan.js'
import { useProgramScope } from '../context/ProgramScopeContext.jsx' 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 { buildProjectsByParent } from '../utils/projectTree.js'
import { projectPath } from '../utils/routes.js' import { projectPath } from '../utils/routes.js'
import { import {
@ -12,9 +11,7 @@ import {
resolvePlanOutlineProjectId, resolvePlanOutlineProjectId,
resolvePlanOutlineActionId, resolvePlanOutlineActionId,
} from '../plan/planOutlineNodes.js' } from '../plan/planOutlineNodes.js'
import { resolvePlanOutlineNodes } from '../config/methodUiDefaults.js' import { resolvePlanOutlineNodesFromProfile } from '../registry/resolveOperatingProfile.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 }) {
@ -55,15 +52,12 @@ function PlanOutlineProjectTree({ projects, parentId, activeProjectId, depth = 0
export function PlanOutlineNav() { export function PlanOutlineNav() {
const location = useLocation() const location = useLocation()
const { initiativeId, initiativeTitle, hrefWithScope } = useProgramScope() const { initiativeId, initiativeTitle, hrefWithScope } = useProgramScope()
const { operatingContext, hasActiveSprint } = useMethodUiContext()
const ops = useOptionalInitiativeOperations()
const activeKey = resolvePlanOutlineActiveKey(location.pathname) const activeKey = resolvePlanOutlineActiveKey(location.pathname)
const activeProjectId = resolvePlanOutlineProjectId(location.pathname) const activeProjectId = resolvePlanOutlineProjectId(location.pathname)
const activeActionId = resolvePlanOutlineActionId(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 [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),
) )
@ -71,6 +65,29 @@ export function PlanOutlineNav() {
activeKey === 'work' || Boolean(activeActionId), 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(() => { useEffect(() => {
setStructureOpen(activeKey === 'structure' || Boolean(activeProjectId)) setStructureOpen(activeKey === 'structure' || Boolean(activeProjectId))
}, [activeKey, activeProjectId]) }, [activeKey, activeProjectId])
@ -79,85 +96,31 @@ 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( const outlineNodes = useMemo(
() => () =>
resolvePlanOutlineNodes({ resolvePlanOutlineNodesFromProfile({
archetypeKey, operatingContext,
hasActiveSprint, hasActiveSprint,
}), }),
[archetypeKey, hasActiveSprint], [operatingContext, hasActiveSprint],
) )
useEffect(() => { useEffect(() => {
if (!initiativeId) { if (!initiativeId) {
setProjects([]) setExecutionGraph(null)
return undefined return undefined
} }
let cancelled = false let cancelled = false
listInitiativeProjects(initiativeId) getInitiativeExecutionGraphState(initiativeId)
.then((items) => { .then((graphState) => {
if (!cancelled) setProjects(Array.isArray(items) ? items : []) if (!cancelled) setExecutionGraph(graphState)
}) })
.catch(() => { .catch(() => {
if (!cancelled) setProjects([]) if (!cancelled) setExecutionGraph(null)
}) })
return () => { return () => {
cancelled = true 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]) }, [initiativeId, location.pathname])
function nodeLabel(node) { function nodeLabel(node) {

View File

@ -10,6 +10,7 @@ import { useParams } from 'react-router-dom'
import { import {
getInitiative, getInitiative,
getInitiativeSteeringSnapshot, getInitiativeSteeringSnapshot,
getInitiativeOperatingContext,
listInitiativeActions, listInitiativeActions,
createInitiativeAction, createInitiativeAction,
updateInitiative, updateInitiative,
@ -86,6 +87,10 @@ import { useOptionalProgramScope } from './ProgramScopeContext.jsx'
import { collectProjectSubtreeIds } from '../utils/projectTree.js' import { collectProjectSubtreeIds } from '../utils/projectTree.js'
import { filterActionsForWorkCycle } from '../utils/workCycleActions.js' import { filterActionsForWorkCycle } from '../utils/workCycleActions.js'
import { listFeatureParentActions } from '../utils/actionReferences.js' import { listFeatureParentActions } from '../utils/actionReferences.js'
import {
applySliceResults,
loadOperatingSlices,
} from '../registry/operatingSliceLoaders.js'
const InitiativeOperationsContext = createContext(null) const InitiativeOperationsContext = createContext(null)
@ -114,6 +119,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
const [steeringSnapshotLoading, setSteeringSnapshotLoading] = useState(false) const [steeringSnapshotLoading, setSteeringSnapshotLoading] = useState(false)
const [steeringSnapshotError, setSteeringSnapshotError] = useState(null) const [steeringSnapshotError, setSteeringSnapshotError] = useState(null)
const [steeringMethods, setSteeringMethods] = useState([]) const [steeringMethods, setSteeringMethods] = useState([])
const [operatingContext, setOperatingContext] = useState(null)
const [methodBusy, setMethodBusy] = useState(false) const [methodBusy, setMethodBusy] = useState(false)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState(null) const [error, setError] = useState(null)
@ -123,62 +129,74 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
const [hideDone, setHideDone] = useState(true) const [hideDone, setHideDone] = useState(true)
const [selectedWorkCycleId, setSelectedWorkCycleId] = useState('') 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 (sessionLoading || !id) return
if (!silent) setLoading(true) if (!silent) setLoading(true)
setError(null) setError(null)
try { try {
const [initData, actionData] = await Promise.all([
getInitiative(id),
listInitiativeActions(id),
])
setInitiative(initData)
setActions(actionData)
const loads = []
const canReadOm = const canReadOm =
capabilities.has('kairo.initiative.read') || capabilities.has('kairo.initiative.read') ||
capabilities.has('kairo.blocker.read') capabilities.has('kairo.blocker.read')
if (canReadOm) {
loads.push(listInitiativeBlockers(id).then(setBlockers).catch(() => setBlockers([]))) let opContext = null
loads.push(listInitiativeBacklog(id).then(setBacklogItems).catch(() => setBacklogItems([]))) if (!sliceKeys) {
loads.push(listInitiativeRoadmapItems(id).then(setRoadmapItems).catch(() => setRoadmapItems([]))) opContext = await getInitiativeOperatingContext(id).catch(() => null)
loads.push(listInitiativeProjects(id).then(setProjects).catch(() => setProjects([]))) setOperatingContext(opContext)
loads.push(listInitiativeEvidence(id).then(setEvidenceItems).catch(() => setEvidenceItems([]))) }
loads.push(listInitiativeDecisions(id).then(setDecisions).catch(() => setDecisions([])))
loads.push(listInitiativeReviews(id).then(setReviews).catch(() => setReviews([]))) const omSlices = canReadOm ? opContext?.data_slices || [] : []
loads.push(listInitiativeRecurring(id).then(setRecurringItems).catch(() => setRecurringItems([]))) const keysToLoad = sliceKeys || ['initiative', ...omSlices]
loads.push(listInitiativeWorkCycles(id).then(setWorkCycles).catch(() => setWorkCycles([])))
loads.push( if (keysToLoad.includes('steering_snapshot')) {
getActiveWorkCycle(id) setSteeringSnapshotLoading(true)
.then((item) => setActiveWorkCycle(item || null)) setSteeringSnapshotError(null)
.catch(() => setActiveWorkCycle(null)) }
)
loads.push( const results = await loadOperatingSlices(id, keysToLoad)
listSteeringMethods().then(setSteeringMethods).catch(() => setSteeringMethods([])) applySliceResults(results, sliceSetters)
)
loads.push( if (keysToLoad.includes('steering_snapshot') && results.steering_snapshot == null) {
(async () => { setSteeringSnapshotError('Snapshot fehlgeschlagen')
setSteeringSnapshotLoading(true) }
setSteeringSnapshotError(null) if (keysToLoad.includes('steering_snapshot')) {
try { setSteeringSnapshotLoading(false)
setSteeringSnapshot(await getInitiativeSteeringSnapshot(id))
} catch (err) {
setSteeringSnapshot(null)
setSteeringSnapshotError(err.message || 'Snapshot fehlgeschlagen')
} finally {
setSteeringSnapshotLoading(false)
}
})()
)
} }
await Promise.all(loads)
} catch (err) { } catch (err) {
setError(err.message) setError(err.message)
setSteeringSnapshotLoading(false)
} finally { } finally {
setLoading(false) setLoading(false)
} }
}, [id, capabilities, sessionLoading]) }, [id, capabilities, sessionLoading, sliceSetters])
useEffect(() => { useEffect(() => {
load() load()
@ -380,6 +398,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
try { try {
const updated = await updateAction(action.id, { work_cycle_id: targetCycleId }) const updated = await updateAction(action.id, { work_cycle_id: targetCycleId })
setActions((prev) => prev.map((a) => (a.id === action.id ? updated : a))) setActions((prev) => prev.map((a) => (a.id === action.id ? updated : a)))
await reloadSlices(['work_cycles'])
} catch (err) { } catch (err) {
setError(err.message) setError(err.message)
} finally { } finally {
@ -975,6 +994,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
steeringSnapshotLoading, steeringSnapshotLoading,
steeringSnapshotError, steeringSnapshotError,
steeringMethods, steeringMethods,
operatingContext,
dataSlices: operatingContext?.data_slices || [],
methodBusy, methodBusy,
loading, loading,
error, error,
@ -993,6 +1014,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
actorsUsedFallback: actorsState.usedFallback, actorsUsedFallback: actorsState.usedFallback,
reloadActors: actorsState.reload, reloadActors: actorsState.reload,
reload: load, reload: load,
reloadSlices,
handleMethodChange, handleMethodChange,
handleMethodProfileChange, handleMethodProfileChange,
handleCreateAction, handleCreateAction,
@ -1053,8 +1075,12 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
) )
} }
export function useOptionalInitiativeOperations() {
return useContext(InitiativeOperationsContext)
}
export function useInitiativeOperations() { export function useInitiativeOperations() {
const ctx = useContext(InitiativeOperationsContext) const ctx = useOptionalInitiativeOperations()
if (!ctx) { if (!ctx) {
throw new Error('useInitiativeOperations must be used within InitiativeOperationsProvider') throw new Error('useInitiativeOperations must be used within InitiativeOperationsProvider')
} }

View File

@ -1,13 +1,6 @@
import { InitiativeOperationsProvider } from '../context/InitiativeOperationsContext.jsx' /**
import { useProgramScope } from '../context/ProgramScopeContext.jsx' * AP2.3c Scope-Gate ohne eigenen Data-Provider (Provider in ProgramScopeContext).
*/
export function ScopedInitiativeProvider({ initiativeId: initiativeIdProp, children }) { export function ScopedInitiativeProvider({ children }) {
const { initiativeId: scopeInitiativeId } = useProgramScope() return children
const initiativeId = initiativeIdProp || scopeInitiativeId
return (
<InitiativeOperationsProvider initiativeId={initiativeId}>
{children}
</InitiativeOperationsProvider>
)
} }

View File

@ -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 (
<InitiativeOperationsProvider initiativeId={initiativeId || null}>
{children}
</InitiativeOperationsProvider>
)
}

View File

@ -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<string, (initiativeId: string) => Promise<unknown>>} */
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<Record<string, unknown>>}
*/
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<string, unknown>} 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<unknown>}
*/
export async function reloadOperatingSlice(sliceKey, initiativeId) {
const loader = SLICE_LOADERS[sliceKey]
if (!loader) return null
return loader(initiativeId)
}