From 82a5b9adeccf9e7e191746345f4e9ddf54901f90 Mon Sep 17 00:00:00 2001
From: Lars
Date: Sun, 12 Jul 2026 16:27:34 +0200
Subject: [PATCH] AP2.2d: B2b Product-Backlog und B3 Sprint-Backlog in UI.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Zeitbox-API im Frontend, Plan/Ausführen Sprint-Ansichten, Backlog-Convert in aktiven Sprint. agile_iteration bei aktiver Zeitbox für continuous_product.
Co-authored-by: Cursor
---
backend/routers/backlog.py | 4 +
backend/services/backlog.py | 13 ++
backend/steering/signals/engine.py | 25 ++-
backend/tests/test_ap22d_sprint_backlog.py | 44 ++++++
backend/version.py | 2 +-
.../Kairo_Implementation_Truth_Table_v0.1.md | 4 +-
docs/product/Kairo_MVP_Execution_Plan_v0.2.md | 2 +-
frontend/package.json | 2 +-
frontend/src/App.jsx | 2 +
frontend/src/api/workCycles.js | 22 +++
frontend/src/components/ActionForm.jsx | 20 +++
frontend/src/components/ActionHubCard.jsx | 4 +
.../src/components/ArchetypeSteeringHints.jsx | 13 +-
frontend/src/components/BacklogSection.jsx | 8 +-
.../src/components/InitiativeActionsHub.jsx | 15 +-
.../src/components/PlanActionsSection.jsx | 8 +-
frontend/src/components/WorkCyclesPanel.jsx | 119 ++++++++++++++
frontend/src/config/modeNav.test.js | 1 +
frontend/src/config/workNav.js | 1 +
.../context/InitiativeOperationsContext.jsx | 61 +++++++-
frontend/src/pages/modes/PlanSprintPage.jsx | 111 +++++++++++++
frontend/src/pages/modes/PlanWorkPage.jsx | 2 +
frontend/src/pages/modes/WorkLayout.jsx | 35 ++++-
frontend/src/pages/modes/WorkSprintPage.jsx | 148 ++++++++++++++++++
frontend/src/plan/planOutlineNodes.js | 1 +
frontend/src/plan/planOutlineNodes.test.js | 2 +
frontend/src/registry/viewRegistry.js | 4 +
27 files changed, 646 insertions(+), 27 deletions(-)
create mode 100644 backend/tests/test_ap22d_sprint_backlog.py
create mode 100644 frontend/src/api/workCycles.js
create mode 100644 frontend/src/components/WorkCyclesPanel.jsx
create mode 100644 frontend/src/pages/modes/PlanSprintPage.jsx
create mode 100644 frontend/src/pages/modes/WorkSprintPage.jsx
diff --git a/backend/routers/backlog.py b/backend/routers/backlog.py
index e2d806c..0d14adc 100644
--- a/backend/routers/backlog.py
+++ b/backend/routers/backlog.py
@@ -34,6 +34,8 @@ class BacklogUpdateRequest(BaseModel):
class BacklogConvertRequest(BaseModel):
assigned_actor_ids: list[str] = Field(default_factory=list)
+ work_cycle_id: Optional[str] = None
+ assign_active_sprint: bool = True
@router.get("/{backlog_item_id}")
@@ -103,6 +105,8 @@ def convert_backlog_to_action(
backlog_item_id=backlog_item_id,
user_id=ctx.user_id,
assigned_actor_ids=assigned,
+ work_cycle_id=body.work_cycle_id,
+ assign_active_sprint=body.assign_active_sprint,
)
except ValueError as exc:
detail = str(exc)
diff --git a/backend/services/backlog.py b/backend/services/backlog.py
index ccb0f78..d509e4b 100644
--- a/backend/services/backlog.py
+++ b/backend/services/backlog.py
@@ -295,6 +295,8 @@ def convert_backlog_to_action(
backlog_item_id: str,
user_id: Optional[str] = None,
assigned_actor_ids: Optional[list[str]] = None,
+ work_cycle_id: Optional[str] = None,
+ assign_active_sprint: bool = True,
) -> dict[str, Any]:
existing = get_backlog_item(tenant_id=tenant_id, backlog_item_id=backlog_item_id)
if not existing:
@@ -304,6 +306,16 @@ def convert_backlog_to_action(
if existing["status"] not in ("accepted", "triaged", "new"):
raise ValueError("Backlog-Item kann in diesem Status nicht konvertiert werden")
+ resolved_cycle_id = work_cycle_id
+ if not resolved_cycle_id and assign_active_sprint:
+ from services.work_cycle import get_active_work_cycle
+
+ active = get_active_work_cycle(
+ tenant_id=tenant_id, initiative_id=existing["initiative_id"]
+ )
+ if active:
+ resolved_cycle_id = active["id"]
+
action = create_action(
tenant_id=tenant_id,
initiative_id=existing["initiative_id"],
@@ -311,6 +323,7 @@ def convert_backlog_to_action(
description=existing["description"] or "",
priority=existing["priority"],
roadmap_item_id=existing.get("roadmap_item_id"),
+ work_cycle_id=resolved_cycle_id,
assigned_actor_ids=assigned_actor_ids or [],
user_id=user_id,
)
diff --git a/backend/steering/signals/engine.py b/backend/steering/signals/engine.py
index a96db70..76e49db 100644
--- a/backend/steering/signals/engine.py
+++ b/backend/steering/signals/engine.py
@@ -25,6 +25,25 @@ def _resolve_method_key(ctx: TenantContext, initiative_id: str | None) -> str:
return "generic_operating"
+def _resolve_next_action_strategy_key(
+ ctx: TenantContext, initiative_id: str | None
+) -> str:
+ method_key = _resolve_method_key(ctx, initiative_id)
+ method = get_method(method_key)
+ strategy_key = method.next_action_strategy_key if method else default_strategy.key
+ if not initiative_id:
+ return strategy_key
+
+ from services.work_cycle import get_active_work_cycle
+
+ active = get_active_work_cycle(tenant_id=ctx.tenant_id, initiative_id=initiative_id)
+ if active and strategy_key == "continuous_product":
+ agile = get_next_action_strategy("agile_iteration")
+ if agile:
+ return agile.key
+ return strategy_key
+
+
def evaluate(
ctx: TenantContext,
kind: SignalKind = "attention",
@@ -35,10 +54,6 @@ def evaluate(
if kind == "attention":
return default_rules.get_attention_items(ctx)
- method_key = _resolve_method_key(ctx, initiative_id)
- method = get_method(method_key)
- strategy_key = (
- method.next_action_strategy_key if method else default_strategy.key
- )
+ strategy_key = _resolve_next_action_strategy_key(ctx, initiative_id)
strategy = get_next_action_strategy(strategy_key) or default_strategy
return strategy.evaluate(ctx, initiative_id=initiative_id, limit=limit)
diff --git a/backend/tests/test_ap22d_sprint_backlog.py b/backend/tests/test_ap22d_sprint_backlog.py
new file mode 100644
index 0000000..7fe9341
--- /dev/null
+++ b/backend/tests/test_ap22d_sprint_backlog.py
@@ -0,0 +1,44 @@
+"""AP2.2d — Backlog convert assigns active sprint."""
+
+from __future__ import annotations
+
+from tests.factories import provision_user_in_tenant
+from tests.test_initiatives_actions import _auth, _create_initiative, _login
+
+
+def test_convert_backlog_assigns_active_work_cycle(client):
+ user = provision_user_in_tenant(tenant_role="admin")
+ token = _login(client, user)
+
+ created = _create_initiative(
+ client,
+ token,
+ title="Product Backlog",
+ archetype_key="initiative.product",
+ )
+ initiative_id = created.json()["id"]
+
+ cycle = client.post(
+ f"/api/initiatives/{initiative_id}/work-cycles",
+ json={"title": "Sprint R1", "status": "active"},
+ headers=_auth(token),
+ )
+ assert cycle.status_code == 201
+ cycle_id = cycle.json()["id"]
+
+ backlog = client.post(
+ f"/api/initiatives/{initiative_id}/backlog",
+ json={"title": "Feature X", "status": "accepted"},
+ headers=_auth(token),
+ )
+ assert backlog.status_code == 201
+ backlog_id = backlog.json()["id"]
+
+ converted = client.post(
+ f"/api/backlog/{backlog_id}/convert-to-action",
+ json={"assign_active_sprint": True},
+ headers=_auth(token),
+ )
+ assert converted.status_code == 201
+ action = converted.json()["action"]
+ assert action["work_cycle_id"] == cycle_id
diff --git a/backend/version.py b/backend/version.py
index 0c74c91..55795b0 100644
--- a/backend/version.py
+++ b/backend/version.py
@@ -1,3 +1,3 @@
-APP_VERSION = "0.20.1-ap2.2bc"
+APP_VERSION = "0.20.2-ap2.2d"
DB_SCHEMA_VERSION = "025"
APP_NAME = "jinkendo-kairo"
diff --git a/docs/product/Kairo_Implementation_Truth_Table_v0.1.md b/docs/product/Kairo_Implementation_Truth_Table_v0.1.md
index 859eb76..b4c40ee 100644
--- a/docs/product/Kairo_Implementation_Truth_Table_v0.1.md
+++ b/docs/product/Kairo_Implementation_Truth_Table_v0.1.md
@@ -148,8 +148,8 @@ Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden.
|----------|------------|------------------|-----------|--------|-------|
| A1 Reifegrad | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2c + AP2.0e |
| A2 Linear | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2b |
-| B2b Product | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2d |
-| B3 Sprint-Profil | ◐ | ◐ | ◐ | ◐ | AP2.2d |
+| B2b Product | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2d ✓ |
+| B3 Sprint-Profil | ◐ | ◐ | ◐ | ◐ | AP2.2d ✓ |
| B2a Programm | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2e |
---
diff --git a/docs/product/Kairo_MVP_Execution_Plan_v0.2.md b/docs/product/Kairo_MVP_Execution_Plan_v0.2.md
index cbed10a..fd1d1ca 100644
--- a/docs/product/Kairo_MVP_Execution_Plan_v0.2.md
+++ b/docs/product/Kairo_MVP_Execution_Plan_v0.2.md
@@ -75,7 +75,7 @@ Phase 1 AP2.2a Archetyp-geführte Anlage + Starter-Kits ✓
Phase 2 AP1.9d Methoden-Default-Ansichten (Anti-Todo-Wand) ◐ (Hints + Recurring)
Phase 3 AP2.2b A2 Linear End-to-End ◐
AP2.2c A1 Reifegrad + AP2.0e ◐ Code
- AP2.2d B2b Product + B3 Sprint
+ AP2.2d B2b Product + B3 Sprint ✓
AP2.2e B2a Programm (optional vor AP2.1)
Phase 4 AP1.7b Operational API — Pflege-Parität
Phase 5 AP2.1 Validation Report v0.3 (alle Stufe-A-Szenarien)
diff --git a/frontend/package.json b/frontend/package.json
index 091a44c..32b3f44 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "kairo-jinkendo-frontend",
- "version": "0.20.1-ap2.2bc",
+ "version": "0.20.2-ap2.2d",
"private": true,
"type": "module",
"scripts": {
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 0c9eec1..74cb635 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -63,6 +63,7 @@ function AppRoutes() {
}>
} />
} />
+ } />
} />
@@ -73,6 +74,7 @@ function AppRoutes() {
} />
} />
} />
+ } />
} />
diff --git a/frontend/src/api/workCycles.js b/frontend/src/api/workCycles.js
new file mode 100644
index 0000000..bf988aa
--- /dev/null
+++ b/frontend/src/api/workCycles.js
@@ -0,0 +1,22 @@
+import { apiFetch } from './client.js'
+
+export function listInitiativeWorkCycles(initiativeId) {
+ return apiFetch(`/api/initiatives/${initiativeId}/work-cycles`)
+}
+
+export function getActiveWorkCycle(initiativeId) {
+ return apiFetch(`/api/initiatives/${initiativeId}/work-cycles/active`)
+}
+
+export function createWorkCycle(initiativeId, body) {
+ return apiFetch(`/api/initiatives/${initiativeId}/work-cycles`, {
+ method: 'POST',
+ body: JSON.stringify(body),
+ })
+}
+
+export function activateWorkCycle(initiativeId, workCycleId) {
+ return apiFetch(`/api/initiatives/${initiativeId}/work-cycles/${workCycleId}/activate`, {
+ method: 'POST',
+ })
+}
diff --git a/frontend/src/components/ActionForm.jsx b/frontend/src/components/ActionForm.jsx
index b65a49c..fb0f9e0 100644
--- a/frontend/src/components/ActionForm.jsx
+++ b/frontend/src/components/ActionForm.jsx
@@ -15,6 +15,8 @@ export function ActionForm({
initial = {},
projects = [],
roadmapItems = [],
+ workCycles = [],
+ activeWorkCycle = null,
actors = [],
actorsLoading = false,
actorsError = null,
@@ -37,6 +39,7 @@ export function ActionForm({
due_at: form.due_at.value ? new Date(form.due_at.value).toISOString() : null,
project_id: form.project_id?.value || undefined,
roadmap_item_id: form.roadmap_item_id?.value || undefined,
+ work_cycle_id: form.work_cycle_id?.value || undefined,
assigned_actor_ids: selected,
})
}
@@ -44,6 +47,9 @@ export function ActionForm({
const defaultActors = initial.assigned_actor_ids || []
const leafProjects = getLeafProjects(projects)
const projectOptions = flattenLeafProjectOptions(projects)
+ const cycleOptions = workCycles.filter((c) => c.status !== 'completed')
+ const defaultCycleId =
+ initial.work_cycle_id || (activeWorkCycle?.id && !initial.id ? activeWorkCycle.id : '')
return (
)
}
diff --git a/frontend/src/components/BacklogSection.jsx b/frontend/src/components/BacklogSection.jsx
index 83967da..da02058 100644
--- a/frontend/src/components/BacklogSection.jsx
+++ b/frontend/src/components/BacklogSection.jsx
@@ -19,6 +19,8 @@ export function BacklogSection({
onConvert,
onDelete,
busy,
+ sectionTitle = 'Product Backlog',
+ sectionLead = 'Eingang vor dem Commit — triagieren, dann in Arbeitspaket (Sprint) umwandeln.',
}) {
const [modalMode, setModalMode] = useState(null)
const [dragItemId, setDragItemId] = useState('')
@@ -106,10 +108,8 @@ export function BacklogSection({
-
Backlog
-
- Reihenfolge per Drag & Drop (Desktop) oder ↑/↓ (Mobile).
-
+
{sectionTitle}
+
{sectionLead}
{canManage && (