diff --git a/frontend/src/components/BacklogIntakeGuide.jsx b/frontend/src/components/BacklogIntakeGuide.jsx
new file mode 100644
index 0000000..8d7b7bd
--- /dev/null
+++ b/frontend/src/components/BacklogIntakeGuide.jsx
@@ -0,0 +1,82 @@
+import { Link } from 'react-router-dom'
+import {
+ buildBacklogIntakeGuideSteps,
+ isBacklogIntakeGuideComplete,
+} from '../utils/backlogIntakeGuide.js'
+import { scopedPath } from '../utils/routes.js'
+
+/**
+ * Geführter Plan-Eingang — Schritte aus Methoden-Vertrag / Backlog-Vokabular.
+ */
+export function BacklogIntakeGuide({
+ initiativeId,
+ vocabulary,
+ sprintCommitEnabled,
+ hasPlanableSprint,
+ hasBacklogItems,
+ hasCommittableItems,
+ hasConvertedItems,
+ directWorkEnabled,
+ compact = false,
+}) {
+ if (!initiativeId) return null
+
+ const steps = buildBacklogIntakeGuideSteps({
+ vocabulary,
+ sprintCommitEnabled,
+ hasPlanableSprint,
+ hasBacklogItems,
+ hasCommittableItems,
+ hasConvertedItems,
+ directWorkEnabled,
+ initiativeId,
+ scopedPath,
+ })
+
+ if (!steps.length) return null
+
+ const complete = isBacklogIntakeGuideComplete(steps)
+
+ if (compact && complete) {
+ return (
+
+ Eingang bereit — Items committen oder{' '}
+ {directWorkEnabled ? (
+ direkt unter Plan → Arbeit
+ ) : (
+ 'committete Arbeit unter Ausführen bearbeiten'
+ )}
+ .
+
+ )
+ }
+
+ return (
+
+
+ So funktioniert der Eingang
+
+
+ {steps.map((step) => (
+ -
+
+ {step.state === 'done' ? '✓' : step.state === 'current' ? '→' : '○'}
+
+
+
{step.title}
+
{step.detail}
+ {step.to && step.linkLabel && step.state !== 'done' && (
+
+ {step.linkLabel}
+
+ )}
+
+
+ ))}
+
+
+ )
+}
diff --git a/frontend/src/components/BacklogSection.jsx b/frontend/src/components/BacklogSection.jsx
index aa671ef..969a4d6 100644
--- a/frontend/src/components/BacklogSection.jsx
+++ b/frontend/src/components/BacklogSection.jsx
@@ -12,6 +12,7 @@ import { backlogKindLabel, resolveBacklogVocabulary } from '../utils/resolveBack
import { useMinWidth } from '../hooks/useMinWidth.js'
import { useInitiativeOperations } from '../context/InitiativeOperationsContext.jsx'
import { hasSteeringElement } from '../registry/steeringElementRegistry.js'
+import { BacklogIntakeGuide } from './BacklogIntakeGuide.jsx'
import { actionTitleById } from '../utils/actionReferences.js'
const PLANNING_STATUSES = new Set(['planned', 'active', 'at_risk'])
@@ -107,6 +108,11 @@ export function BacklogSection({
}, [epicItems])
const defaultKind = vocabulary.default_kind || 'story'
+ const hasConvertedItems = items.some((item) => item.status === 'converted')
+ const directWorkEnabled = Boolean(
+ operatingContext?.ui_profile?.planOutlineKeys?.includes('work'),
+ )
+ const guideCompact = sortedItems.length > 0 && (showSprintPlanning || hasConvertedItems)
function closeModal() {
setModalMode(null)
@@ -247,6 +253,20 @@ export function BacklogSection({
)}
+ {canManage && (
+ 0}
+ hasBacklogItems={sortedItems.length > 0}
+ hasCommittableItems={committableItems.length > 0}
+ hasConvertedItems={hasConvertedItems}
+ directWorkEnabled={directWorkEnabled}
+ compact={guideCompact}
+ />
+ )}
+
{sprintCommitEnabled && canManage && !showSprintPlanning && (
@@ -295,10 +315,13 @@ export function BacklogSection({
)}
- {items.length === 0 && }
+ {sortedItems.length === 0 && !hasConvertedItems && (
+
+ )}
-
- {displayRows.map(({ item, depth }) => {
+ {(sortedItems.length > 0 || hasConvertedItems) && (
+
+ {displayRows.map(({ item, depth }) => {
const isDragging = dragItemId === item.id
const isDropTarget = dropTargetId === item.id && dragItemId && dragItemId !== item.id
const reorderable = canReorder && item.status !== 'converted'
@@ -415,18 +438,19 @@ export function BacklogSection({
)
})}
- {items
- .filter((item) => item.status === 'converted')
- .map((item) => (
- -
-
-
{item.title}
-
Konvertiert — nicht mehr sortierbar
-
-
-
- ))}
-
+ {items
+ .filter((item) => item.status === 'converted')
+ .map((item) => (
+ -
+
+
{item.title}
+
Konvertiert — nicht mehr sortierbar
+
+
+
+ ))}
+
+ )}
{modalMode?.kind === 'create' && (
diff --git a/frontend/src/components/PlanSteeringSummary.jsx b/frontend/src/components/PlanSteeringSummary.jsx
new file mode 100644
index 0000000..9b6ee76
--- /dev/null
+++ b/frontend/src/components/PlanSteeringSummary.jsx
@@ -0,0 +1,110 @@
+import { Link } from 'react-router-dom'
+import { useInitiativeOperations } from '../context/InitiativeOperationsContext.jsx'
+import {
+ backlogVocabularyProfileLabel,
+ planSteeringElementLabels,
+} from '../registry/steeringElementRegistry.js'
+import { methodLabelForKey } from '../utils/archetypes.js'
+import { resolveBacklogVocabulary } from '../utils/resolveBacklogVocabulary.js'
+import { scopedPath } from '../utils/routes.js'
+
+const COMPOSITION_MODIFIER_LABELS = {
+ agile_iteration: 'Agile Iteration (aktiver Sprint)',
+}
+
+/**
+ * Kurzüberblick Steuerung auf Plan-Profil — aus Operating Context, nicht Archetyp-If.
+ */
+export function PlanSteeringSummary({ initiativeId }) {
+ const {
+ operatingContext,
+ steeringElements,
+ steeringMethods,
+ activeWorkCycle,
+ } = useInitiativeOperations()
+
+ if (!operatingContext) return null
+
+ const methodKey = operatingContext.method_key
+ const methodLabel = methodLabelForKey(methodKey, steeringMethods)
+ const vocabulary = resolveBacklogVocabulary(operatingContext.backlog_vocabulary)
+ const vocabLabel = backlogVocabularyProfileLabel(vocabulary.profile_key)
+ const elementLabels = planSteeringElementLabels(steeringElements)
+ const compositionKey = operatingContext.active_composition_modifier
+ const compositionLabel = compositionKey
+ ? COMPOSITION_MODIFIER_LABELS[compositionKey] || compositionKey
+ : null
+
+ const planOutlineKeys = operatingContext.ui_profile?.planOutlineKeys || []
+ const showsDirectWork = planOutlineKeys.includes('work')
+ const showsInbox = planOutlineKeys.includes('inbox')
+
+ return (
+
+ Steuerung (aus Methode)
+
+
+
- Methode
+ -
+ {methodLabel}
+ {compositionLabel && (
+ · {compositionLabel}
+ )}
+
+
+ {vocabLabel && vocabulary.kinds?.length > 0 && (
+
+
- Eingang
+ - {vocabLabel}
+
+ )}
+ {elementLabels.length > 0 && (
+
+
- Aktive Elemente
+
-
+
+ {elementLabels.map((label) => (
+ -
+ {label}
+
+ ))}
+
+
+
+ )}
+ {activeWorkCycle?.title && (
+
+
- Aktiver Sprint
+ -
+ {activeWorkCycle.title}
+
+
+ )}
+ {(showsInbox || showsDirectWork) && (
+
+
- Commit-Pfade
+ -
+ {showsInbox && (
+
+ A:{' '}
+ Eingang → Commit
+
+ )}
+ {showsInbox && showsDirectWork && ' · '}
+ {showsDirectWork && (
+
+ B:{' '}
+ Plan → Arbeit (direct)
+
+ )}
+
+
+ )}
+
+
+ Details und Methodenwechsel unter{' '}
+ Kontrolle → Steuerung.
+
+
+ )
+}
diff --git a/frontend/src/pages/modes/PlanProfilePage.jsx b/frontend/src/pages/modes/PlanProfilePage.jsx
index a0181b7..e678a38 100644
--- a/frontend/src/pages/modes/PlanProfilePage.jsx
+++ b/frontend/src/pages/modes/PlanProfilePage.jsx
@@ -8,6 +8,7 @@ import { LoadingState } from '../../components/LoadingState.jsx'
import { Modal } from '../../components/Modal.jsx'
import { InitiativeForm } from '../../components/InitiativeForm.jsx'
import { InitiativeSteeringMeta } from '../../components/InitiativeSteeringMeta.jsx'
+import { PlanSteeringSummary } from '../../components/PlanSteeringSummary.jsx'
import { PriorityBadge } from '../../components/PriorityBadge.jsx'
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
@@ -167,6 +168,7 @@ function PlanProfileInner() {
steeringSnapshot={steeringSnapshot}
steeringMethods={steeringMethods}
/>
+
Verantwortlich (Owner)
{ownerName}
diff --git a/frontend/src/registry/steeringElementRegistry.js b/frontend/src/registry/steeringElementRegistry.js
index 0456557..ef56272 100644
--- a/frontend/src/registry/steeringElementRegistry.js
+++ b/frontend/src/registry/steeringElementRegistry.js
@@ -2,7 +2,7 @@
* AP2.4 — Steuerungselement-Registry (Element → UI-Semantik).
*/
-/** @type {Record
} */
+/** @type {Record} */
export const STEERING_ELEMENT_UI = {
critical_path: {
nextActionTitle: 'Nächster Schritt am kritischen Pfad',
@@ -18,6 +18,40 @@ export const STEERING_ELEMENT_UI = {
},
}
+/** Labels für Steuerungselemente auf Plan-Profil (Anzeige, nicht Verhalten). */
+export const STEERING_ELEMENT_PLAN_LABELS = {
+ work_cycle_scope: 'Sprint-Zeitbox',
+ backlog_epic_hierarchy: 'Epic-Hierarchie im Eingang',
+ gate_fulfillment: 'Gate-Fortschritt',
+ queue_inbox: 'Queue-Eingang',
+ critical_path: 'Kritischer Pfad',
+ recurring_rhythm: 'Rhythmen',
+ maturity_stage: 'Reifegrad-Stufen',
+ chapter_progression: 'Kapitel-Fortschritt',
+ dispute_timeline: 'Verfahren / Fristen',
+ next_action_primary: 'Nächster Schritt',
+}
+
+export const BACKLOG_VOCABULARY_PROFILE_LABELS = {
+ agile_intake: 'Agile Eingang (Epic, Story, Bug, Issue)',
+ continuous_intake: 'Kontinuierlicher Eingang (Idee, Bug, Störung)',
+ standard_intake: 'Standard-Eingang (Story, Bug, Issue)',
+ none: 'Kein Backlog-Eingang',
+}
+
+/** Plan-relevante Elemente zuerst. */
+const PLAN_ELEMENT_ORDER = [
+ 'work_cycle_scope',
+ 'backlog_epic_hierarchy',
+ 'gate_fulfillment',
+ 'queue_inbox',
+ 'critical_path',
+ 'recurring_rhythm',
+ 'maturity_stage',
+ 'chapter_progression',
+ 'dispute_timeline',
+]
+
/**
* @param {string[] | undefined | null} elements
* @param {string} key
@@ -34,3 +68,23 @@ export function steeringElementUi(elements, key) {
if (!hasSteeringElement(elements, key)) return null
return STEERING_ELEMENT_UI[key] || null
}
+
+/**
+ * @param {string[] | undefined | null} elements
+ */
+export function planSteeringElementLabels(elements) {
+ if (!Array.isArray(elements)) return []
+ const order = new Map(PLAN_ELEMENT_ORDER.map((key, index) => [key, index]))
+ return elements
+ .filter((key) => STEERING_ELEMENT_PLAN_LABELS[key])
+ .sort((a, b) => (order.get(a) ?? 99) - (order.get(b) ?? 99))
+ .map((key) => STEERING_ELEMENT_PLAN_LABELS[key])
+}
+
+/**
+ * @param {string | undefined | null} profileKey
+ */
+export function backlogVocabularyProfileLabel(profileKey) {
+ if (!profileKey) return null
+ return BACKLOG_VOCABULARY_PROFILE_LABELS[profileKey] || profileKey
+}
diff --git a/frontend/src/styles/program-chrome.css b/frontend/src/styles/program-chrome.css
index 1d4a0ac..d0aaae7 100644
--- a/frontend/src/styles/program-chrome.css
+++ b/frontend/src/styles/program-chrome.css
@@ -887,3 +887,130 @@
.next-action-reason {
margin-left: 4px;
}
+
+.backlog-intake-guide {
+ margin-bottom: 16px;
+ padding: 12px 14px;
+ border-left: 3px solid var(--jk-primary);
+ background: var(--jk-surface-muted, rgba(0, 0, 0, 0.03));
+ border-radius: 8px;
+}
+
+.backlog-intake-guide--compact {
+ margin-bottom: 12px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ font-size: 14px;
+}
+
+.backlog-intake-guide__title {
+ margin: 0 0 10px;
+}
+
+.backlog-intake-guide__steps {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.backlog-intake-guide__step {
+ display: flex;
+ gap: 10px;
+ align-items: flex-start;
+}
+
+.backlog-intake-guide__step-marker {
+ flex-shrink: 0;
+ width: 1.25rem;
+ font-weight: 700;
+ line-height: 1.4;
+}
+
+.backlog-intake-guide__step--current .backlog-intake-guide__step-marker {
+ color: var(--jk-primary);
+}
+
+.backlog-intake-guide__step--done .backlog-intake-guide__step-marker {
+ color: var(--jk-success, #2e7d32);
+}
+
+.backlog-intake-guide__step--pending {
+ opacity: 0.72;
+}
+
+.backlog-intake-guide__step-detail {
+ margin: 4px 0 0;
+ font-size: 13px;
+}
+
+.backlog-intake-guide__step-link {
+ display: inline-block;
+ margin-top: 4px;
+ font-size: 13px;
+}
+
+.plan-steering-summary {
+ margin: 16px 0;
+ padding: 14px 16px;
+ border-radius: 8px;
+ background: var(--jk-surface-muted, rgba(0, 0, 0, 0.03));
+ border: 1px solid var(--jk-border, rgba(0, 0, 0, 0.08));
+}
+
+.plan-steering-summary__title {
+ margin: 0 0 12px;
+ font-size: 1rem;
+}
+
+.plan-steering-summary__list {
+ margin: 0;
+}
+
+.plan-steering-summary__row {
+ display: grid;
+ grid-template-columns: minmax(7rem, 9rem) 1fr;
+ gap: 8px 12px;
+ margin-bottom: 10px;
+}
+
+.plan-steering-summary__row dt {
+ margin: 0;
+}
+
+.plan-steering-summary__row dd {
+ margin: 0;
+}
+
+.plan-steering-summary__pills {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.plan-steering-summary__pill {
+ padding: 2px 8px;
+ border-radius: 999px;
+ font-size: 12px;
+ background: var(--jk-surface, #fff);
+ border: 1px solid var(--jk-border, rgba(0, 0, 0, 0.1));
+}
+
+.plan-steering-summary__paths {
+ font-size: 14px;
+}
+
+.plan-steering-summary__footnote {
+ margin: 12px 0 0;
+ font-size: 13px;
+}
+
+.plan-steering-summary__tag {
+ font-size: 13px;
+}
diff --git a/frontend/src/utils/backlogIntakeGuide.js b/frontend/src/utils/backlogIntakeGuide.js
new file mode 100644
index 0000000..e0e301f
--- /dev/null
+++ b/frontend/src/utils/backlogIntakeGuide.js
@@ -0,0 +1,119 @@
+/**
+ * Geführter Eingangs-Flow — Schritte aus Operating Context, keine Archetyp-Ifs.
+ */
+
+/**
+ * @typedef {'pending' | 'current' | 'done'} GuideStepState
+ * @typedef {{
+ * id: string,
+ * title: string,
+ * detail: string,
+ * state: GuideStepState,
+ * to?: string,
+ * linkLabel?: string,
+ * }} BacklogIntakeGuideStep
+ */
+
+/**
+ * @param {{
+ * vocabulary: import('./resolveBacklogVocabulary.js').BacklogVocabulary,
+ * sprintCommitEnabled: boolean,
+ * hasPlanableSprint: boolean,
+ * hasBacklogItems: boolean,
+ * hasCommittableItems: boolean,
+ * hasConvertedItems: boolean,
+ * directWorkEnabled: boolean,
+ * initiativeId: string,
+ * scopedPath: (path: string, opts: { initiativeId: string }) => string,
+ * }} input
+ * @returns {BacklogIntakeGuideStep[]}
+ */
+export function buildBacklogIntakeGuideSteps(input) {
+ const {
+ vocabulary,
+ sprintCommitEnabled,
+ hasPlanableSprint,
+ hasBacklogItems,
+ hasCommittableItems,
+ hasConvertedItems,
+ directWorkEnabled,
+ initiativeId,
+ scopedPath,
+ } = input
+
+ const epicHierarchy = Boolean(
+ vocabulary.capabilities?.epic_hierarchy ?? vocabulary.epic_hierarchy,
+ )
+ const storyLabel = vocabulary.labels?.story || 'Story'
+ const epicLabel = vocabulary.labels?.epic || 'Epic'
+
+ /** @type {BacklogIntakeGuideStep[]} */
+ const steps = []
+
+ if (sprintCommitEnabled) {
+ steps.push({
+ id: 'sprint',
+ title: 'Sprint anlegen',
+ detail: 'Unter Plan → Sprint einen Zeitrahmen anlegen (geplant oder aktiv).',
+ state: hasPlanableSprint ? 'done' : 'current',
+ to: scopedPath('/plan/sprint', { initiativeId }),
+ linkLabel: 'Zu Sprint-Planung',
+ })
+ }
+
+ steps.push({
+ id: 'intake',
+ title: epicHierarchy
+ ? `${epicLabel} oder ${storyLabel} im Eingang`
+ : `${storyLabel} im Eingang erfassen`,
+ detail: epicHierarchy
+ ? `Optional ${epicLabel} als Container, darunter ${storyLabel}/Bug/Issue zum Refinement.`
+ : 'Ideen und Störungen sammeln, bevor sie committet werden.',
+ state: hasBacklogItems
+ ? 'done'
+ : steps.some((step) => step.state === 'current')
+ ? 'pending'
+ : 'current',
+ to: undefined,
+ linkLabel: undefined,
+ })
+
+ steps.push({
+ id: 'commit',
+ title: sprintCommitEnabled ? 'In Sprint committen' : 'In Arbeitspaket umwandeln',
+ detail: sprintCommitEnabled
+ ? 'Item markieren → Ziel-Sprint wählen → committen. Daraus entsteht ein Arbeitspaket im Ist.'
+ : 'Freigegebenes Item committen — es wird ein Arbeitspaket im Ist.',
+ state: hasConvertedItems
+ ? 'done'
+ : hasCommittableItems && (!sprintCommitEnabled || hasPlanableSprint)
+ ? 'current'
+ : 'pending',
+ })
+
+ if (directWorkEnabled) {
+ steps.push({
+ id: 'direct-ap',
+ title: 'Alternativ: Arbeitspaket direkt',
+ detail:
+ 'Ad-hoc-Spike oder Nacharbeit ohne Backlog-Umweg — Plan → Arbeit (Direct Commit, Pfad B).',
+ state: 'pending',
+ to: scopedPath('/plan/work', { initiativeId }),
+ linkLabel: 'Plan → Arbeit',
+ })
+ }
+
+ if (!steps.some((step) => step.state === 'current')) {
+ const firstPending = steps.find((step) => step.state === 'pending')
+ if (firstPending) firstPending.state = 'current'
+ }
+
+ return steps
+}
+
+/**
+ * @param {BacklogIntakeGuideStep[]} steps
+ */
+export function isBacklogIntakeGuideComplete(steps) {
+ return steps.length > 0 && steps.every((step) => step.state === 'done' || step.id === 'direct-ap')
+}
diff --git a/frontend/src/utils/backlogIntakeGuide.test.js b/frontend/src/utils/backlogIntakeGuide.test.js
new file mode 100644
index 0000000..615cdf4
--- /dev/null
+++ b/frontend/src/utils/backlogIntakeGuide.test.js
@@ -0,0 +1,44 @@
+import { describe, expect, it } from 'vitest'
+import { buildBacklogIntakeGuideSteps } from './backlogIntakeGuide.js'
+import { DEFAULT_BACKLOG_VOCABULARY } from './resolveBacklogVocabulary.js'
+
+const scope = (path, { initiativeId }) => `${path}?initiative=${initiativeId}`
+
+describe('buildBacklogIntakeGuideSteps', () => {
+ it('startet mit Sprint-Schritt wenn sprint_commit aktiv', () => {
+ const steps = buildBacklogIntakeGuideSteps({
+ vocabulary: {
+ ...DEFAULT_BACKLOG_VOCABULARY,
+ capabilities: { sprint_commit: true, epic_hierarchy: true },
+ labels: { epic: 'Epic', story: 'Story' },
+ },
+ sprintCommitEnabled: true,
+ hasPlanableSprint: false,
+ hasBacklogItems: false,
+ hasCommittableItems: false,
+ hasConvertedItems: false,
+ directWorkEnabled: true,
+ initiativeId: 'abc',
+ scopedPath: scope,
+ })
+ expect(steps[0].id).toBe('sprint')
+ expect(steps[0].state).toBe('current')
+ expect(steps.some((step) => step.id === 'direct-ap')).toBe(true)
+ })
+
+ it('markiert Eingang als erledigt wenn Items vorhanden', () => {
+ const steps = buildBacklogIntakeGuideSteps({
+ vocabulary: DEFAULT_BACKLOG_VOCABULARY,
+ sprintCommitEnabled: false,
+ hasPlanableSprint: false,
+ hasBacklogItems: true,
+ hasCommittableItems: true,
+ hasConvertedItems: false,
+ directWorkEnabled: false,
+ initiativeId: 'abc',
+ scopedPath: scope,
+ })
+ const intake = steps.find((step) => step.id === 'intake')
+ expect(intake?.state).toBe('done')
+ })
+})