From b7d88ce24a5cf8f80535a7a2a0e2e8acdf2d9bfd Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 28 Jul 2026 12:32:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(A1):=20Minimum=20Usability=20Slice=20?= =?UTF-8?q?=E2=80=94=20CadenceInstance,=20Today-=C3=9Cbung,=20Gate-Fortsch?= =?UTF-8?q?ritt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PO No-Go A1 adressieren: Tages-Tracking mit Messnotiz, Work-UI für Übung, Next-Action-Link, Gate-Fortschritt in Kontrolle, Eingang aus A1-Nav entfernt. Co-authored-by: Cursor --- backend/entity_archetypes/ui_profiles.py | 2 +- backend/migrations/030_cadence_instances.sql | 25 ++ backend/routers/recurring.py | 27 +++ backend/services/cadence_instance.py | 220 +++++++++++++++++ backend/services/recurring.py | 16 +- .../next_action/recurring_helpers.py | 10 +- backend/tests/test_a1_cadence_mvp.py | 70 ++++++ backend/tests/test_ap23a_operating_context.py | 2 + .../Kairo_Implementation_Truth_Table_v0.1.md | 11 +- docs/product/Kairo_MVP_Execution_Plan_v0.2.md | 2 + .../SPEC_A1_maturity_journey_v0.1.md | 5 + ...print1_AP2_1_MVP_Validation_Report_v0.3.md | 44 +++- ...AP2_1_Validation_Findings_Register_v0.1.md | 229 ++++++++++++++++++ ...int1_AP_A1_Minimum_Usability_Slice_v0.1.md | 61 +++++ frontend/src/api/recurring.js | 7 + .../src/components/MaturityStagePanel.jsx | 62 ++++- .../src/components/WorkTodayPracticePanel.jsx | 99 ++++++++ .../InitiativeCompositionSurface.jsx | 2 + .../src/composition/compositionProviders.js | 10 + .../src/composition/providerComponents.jsx | 2 + .../composition/resolveSteeringComposition.js | 12 + .../resolveSteeringComposition.test.js | 26 +- frontend/src/config/methodUiDefaults.js | 2 +- frontend/src/config/methodUiDefaults.test.js | 11 + frontend/src/widgets/NextActionWidget.jsx | 10 + 25 files changed, 936 insertions(+), 31 deletions(-) create mode 100644 backend/migrations/030_cadence_instances.sql create mode 100644 backend/services/cadence_instance.py create mode 100644 backend/tests/test_a1_cadence_mvp.py create mode 100644 docs/sprints/Sprint1_AP2_1_Validation_Findings_Register_v0.1.md create mode 100644 docs/sprints/Sprint1_AP_A1_Minimum_Usability_Slice_v0.1.md create mode 100644 frontend/src/components/WorkTodayPracticePanel.jsx diff --git a/backend/entity_archetypes/ui_profiles.py b/backend/entity_archetypes/ui_profiles.py index 9d5d606..d33e2b6 100644 --- a/backend/entity_archetypes/ui_profiles.py +++ b/backend/entity_archetypes/ui_profiles.py @@ -124,7 +124,7 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = { "planDefaultRoute": "/plan/gates", "workDefaultRoute": "/work/today", "controlDefaultRoute": "/control/status", - "planOutlineKeys": ["profile", "gates", "inbox", "work"], + "planOutlineKeys": ["profile", "gates", "work"], "workNavKeys": ["today", "mine", "sprint"], "dataSlices": [ "actions", diff --git a/backend/migrations/030_cadence_instances.sql b/backend/migrations/030_cadence_instances.sql new file mode 100644 index 0000000..02e7700 --- /dev/null +++ b/backend/migrations/030_cadence_instances.sql @@ -0,0 +1,25 @@ +-- AP A1.1 — CadenceInstance (minimal): eine offene Instanz pro Recurring, Tages-Tracking + +CREATE TABLE cadence_instances ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + recurring_element_id UUID NOT NULL REFERENCES recurring_elements(id) ON DELETE CASCADE, + status VARCHAR(32) NOT NULL DEFAULT 'open' + CHECK (status IN ('open', 'completed')), + due_at TIMESTAMPTZ NOT NULL, + completed_at TIMESTAMPTZ NULL, + measurement_note TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_cadence_instances_tenant_recurring + ON cadence_instances(tenant_id, recurring_element_id); + +CREATE INDEX idx_cadence_instances_open_due + ON cadence_instances(tenant_id, due_at) + WHERE status = 'open'; + +CREATE UNIQUE INDEX idx_cadence_instances_one_open_per_recurring + ON cadence_instances(recurring_element_id) + WHERE status = 'open'; diff --git a/backend/routers/recurring.py b/backend/routers/recurring.py index 5c40f06..a4768cd 100644 --- a/backend/routers/recurring.py +++ b/backend/routers/recurring.py @@ -24,6 +24,10 @@ class RecurringUpdateRequest(BaseModel): clear_next_due_at: bool = False +class RecurringCompleteRequest(BaseModel): + measurement_note: Optional[str] = Field(default=None, max_length=2000) + + def _parse_datetime(value: Optional[str]) -> Optional[datetime]: if value is None: return None @@ -73,6 +77,29 @@ def update_recurring_element( return item +@router.post("/{recurring_id}/complete") +def complete_recurring_practice( + recurring_id: str, + body: RecurringCompleteRequest, + ctx: TenantContext = Depends(require_capability("kairo.recurring.manage")), +): + from services.cadence_instance import complete_open_instance + + try: + result = complete_open_instance( + tenant_id=ctx.tenant_id, + recurring_element_id=recurring_id, + user_id=ctx.user_id or None, + measurement_note=body.measurement_note or "", + ) + except ValueError as exc: + detail = str(exc) + if detail == "Recurring-Element nicht gefunden": + raise HTTPException(status_code=404, detail=detail) from exc + raise HTTPException(status_code=400, detail=detail) from exc + return result + + @router.delete("/{recurring_id}", status_code=204) def delete_recurring_element( recurring_id: str, diff --git a/backend/services/cadence_instance.py b/backend/services/cadence_instance.py new file mode 100644 index 0000000..1ba3cce --- /dev/null +++ b/backend/services/cadence_instance.py @@ -0,0 +1,220 @@ +"""CadenceInstance service — AP A1.1 minimal daily practice tracking.""" + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from typing import Any, Literal, Optional + +from psycopg2.extras import RealDictCursor + +from db import get_connection +from services.audit import log_audit + +CadenceStatus = Literal["open", "completed"] + +_CADENCE_COLUMNS = """ + id, tenant_id, recurring_element_id, status, due_at, + completed_at, measurement_note, created_at, updated_at +""" + + +def _serialize_row(row: dict[str, Any]) -> dict[str, Any]: + result = dict(row) + for key in ("id", "tenant_id", "recurring_element_id"): + if result.get(key): + result[key] = str(result[key]) + for ts_key in ("due_at", "completed_at", "created_at", "updated_at"): + if result.get(ts_key): + result[ts_key] = result[ts_key].isoformat() + return result + + +def get_open_instance( + *, tenant_id: str, recurring_element_id: str +) -> Optional[dict[str, Any]]: + conn = get_connection() + try: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute( + f""" + SELECT {_CADENCE_COLUMNS} + FROM cadence_instances + WHERE tenant_id = %s + AND recurring_element_id = %s + AND status = 'open' + LIMIT 1 + """, + (tenant_id, recurring_element_id), + ) + row = cur.fetchone() + return _serialize_row(dict(row)) if row else None + finally: + conn.close() + + +def ensure_open_instance( + *, + tenant_id: str, + recurring_element_id: str, + due_at: Optional[datetime] = None, +) -> dict[str, Any]: + """Ensure exactly one open CadenceInstance (idempotent).""" + existing = get_open_instance( + tenant_id=tenant_id, recurring_element_id=recurring_element_id + ) + if existing: + return existing + + due = due_at or datetime.now(timezone.utc) + conn = get_connection() + try: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute( + f""" + INSERT INTO cadence_instances ( + tenant_id, recurring_element_id, status, due_at + ) + VALUES (%s, %s, 'open', %s) + RETURNING {_CADENCE_COLUMNS} + """, + (tenant_id, recurring_element_id, due), + ) + row = _serialize_row(dict(cur.fetchone())) + conn.commit() + finally: + conn.close() + return row + + +def complete_open_instance( + *, + tenant_id: str, + recurring_element_id: str, + user_id: Optional[str] = None, + measurement_note: str = "", +) -> dict[str, Any]: + """ + Close open instance, advance recurring next_due_at, open next instance. + Returns { completed, recurring, next_instance }. + """ + from services.recurring import get_recurring_element, update_recurring_element + + recurring = get_recurring_element( + tenant_id=tenant_id, recurring_id=recurring_element_id + ) + if not recurring: + raise ValueError("Recurring-Element nicht gefunden") + if recurring["status"] != "active": + raise ValueError("Recurring-Element ist nicht aktiv") + + open_inst = get_open_instance( + tenant_id=tenant_id, recurring_element_id=recurring_element_id + ) + if not open_inst: + open_inst = ensure_open_instance( + tenant_id=tenant_id, + recurring_element_id=recurring_element_id, + due_at=datetime.now(timezone.utc), + ) + + now = datetime.now(timezone.utc) + note = (measurement_note or "").strip() + + conn = get_connection() + try: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute( + f""" + UPDATE cadence_instances + SET status = 'completed', + completed_at = %s, + measurement_note = %s, + updated_at = NOW() + WHERE id = %s AND tenant_id = %s AND status = 'open' + RETURNING {_CADENCE_COLUMNS} + """, + (now, note, open_inst["id"], tenant_id), + ) + completed_row = cur.fetchone() + if not completed_row: + raise ValueError("Keine offene Cadence-Instanz") + completed = _serialize_row(dict(completed_row)) + conn.commit() + finally: + conn.close() + + interval = recurring.get("interval_days") or 1 + next_due = now + timedelta(days=int(interval)) + + update_recurring_element( + tenant_id=tenant_id, + recurring_id=recurring_element_id, + user_id=user_id, + next_due_at=next_due, + ) + + next_instance = ensure_open_instance( + tenant_id=tenant_id, + recurring_element_id=recurring_element_id, + due_at=next_due, + ) + + log_audit( + "cadence_instance.completed", + user_id=user_id, + tenant_id=tenant_id, + details={ + "cadence_instance_id": completed["id"], + "recurring_id": recurring_element_id, + "measurement_note": note[:200] if note else "", + }, + ) + + updated_recurring = get_recurring_element( + tenant_id=tenant_id, recurring_id=recurring_element_id + ) + return { + "completed_instance": completed, + "next_instance": next_instance, + "recurring": updated_recurring, + } + + +def attach_open_instances_to_recurring_list( + items: list[dict[str, Any]], *, tenant_id: str +) -> list[dict[str, Any]]: + if not items: + return items + ids = [item["id"] for item in items] + conn = get_connection() + try: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute( + f""" + SELECT {_CADENCE_COLUMNS} + FROM cadence_instances + WHERE tenant_id = %s + AND recurring_element_id = ANY(%s::uuid[]) + AND status = 'open' + """, + (tenant_id, ids), + ) + by_recurring = { + str(row["recurring_element_id"]): _serialize_row(dict(row)) + for row in cur.fetchall() + } + finally: + conn.close() + + enriched = [] + for item in items: + copy = dict(item) + open_inst = by_recurring.get(item["id"]) + if not open_inst and item.get("status") == "active": + open_inst = ensure_open_instance( + tenant_id=tenant_id, + recurring_element_id=item["id"], + ) + copy["open_cadence_instance"] = open_inst + enriched.append(copy) + return enriched diff --git a/backend/services/recurring.py b/backend/services/recurring.py index f429b98..2075d16 100644 --- a/backend/services/recurring.py +++ b/backend/services/recurring.py @@ -90,6 +90,16 @@ def create_recurring_element( tenant_id=tenant_id, details={"recurring_id": row["id"], "initiative_id": initiative_id}, ) + + from services.cadence_instance import ensure_open_instance + + due = next_due_at or datetime.now(timezone.utc) + open_inst = ensure_open_instance( + tenant_id=tenant_id, + recurring_element_id=row["id"], + due_at=due, + ) + row["open_cadence_instance"] = open_inst return row @@ -111,10 +121,14 @@ def list_recurring_for_initiative( """, (tenant_id, initiative_id), ) - return [_serialize_row(dict(r)) for r in cur.fetchall()] + rows = [_serialize_row(dict(r)) for r in cur.fetchall()] finally: conn.close() + from services.cadence_instance import attach_open_instances_to_recurring_list + + return attach_open_instances_to_recurring_list(rows, tenant_id=tenant_id) + def get_recurring_element( *, tenant_id: str, recurring_id: str diff --git a/backend/steering/strategies/next_action/recurring_helpers.py b/backend/steering/strategies/next_action/recurring_helpers.py index b265cdc..acba6fc 100644 --- a/backend/steering/strategies/next_action/recurring_helpers.py +++ b/backend/steering/strategies/next_action/recurring_helpers.py @@ -22,10 +22,11 @@ def recurring_due_candidates( re.title AS title, 'Wiederkehrendes Element fällig' AS summary, re.initiative_id, + re.id AS recurring_id, NULL::uuid AS action_id, NULL::uuid AS backlog_item_id, 'recurring_due' AS reason_code, - 'Rhythmus-Element bearbeiten' AS recommended_action + 'Übung heute erledigen' AS recommended_action FROM recurring_elements re WHERE re.tenant_id = %s AND re.initiative_id = %s AND re.status = 'active' @@ -40,6 +41,8 @@ def recurring_due_candidates( for row in cur.fetchall(): item = dict(row) item["initiative_id"] = str(item["initiative_id"]) + if item.get("recurring_id"): + item["recurring_id"] = str(item["recurring_id"]) items.append(item) return items finally: @@ -60,10 +63,11 @@ def active_recurring_candidates( re.title AS title, 'Heutige Übung / Routine' AS summary, re.initiative_id, + re.id AS recurring_id, NULL::uuid AS action_id, NULL::uuid AS backlog_item_id, 'recurring_active' AS reason_code, - 'Routine ausführen' AS recommended_action + 'Übung heute erledigen' AS recommended_action FROM recurring_elements re WHERE re.tenant_id = %s AND re.initiative_id = %s AND re.status = 'active' @@ -76,6 +80,8 @@ def active_recurring_candidates( for row in cur.fetchall(): item = dict(row) item["initiative_id"] = str(item["initiative_id"]) + if item.get("recurring_id"): + item["recurring_id"] = str(item["recurring_id"]) items.append(item) return items finally: diff --git a/backend/tests/test_a1_cadence_mvp.py b/backend/tests/test_a1_cadence_mvp.py new file mode 100644 index 0000000..10b6eee --- /dev/null +++ b/backend/tests/test_a1_cadence_mvp.py @@ -0,0 +1,70 @@ +"""AP A1.1 — CadenceInstance complete practice flow.""" + +from __future__ import annotations + +from tests.factories import provision_user_in_tenant +from tests.test_initiatives_actions import _auth, _create_initiative, _login + + +def test_complete_daily_practice_advances_cadence(client): + user = provision_user_in_tenant(tenant_role="admin") + token = _login(client, user) + + created = _create_initiative( + client, + token, + title="Spagat Cadence", + archetype_key="initiative.maturity_journey", + ) + initiative_id = created.json()["id"] + + recurring = client.get( + f"/api/initiatives/{initiative_id}/recurring", + headers=_auth(token), + ).json() + assert recurring + item = recurring[0] + assert item.get("open_cadence_instance") + assert item["open_cadence_instance"]["status"] == "open" + recurring_id = item["id"] + + completed = client.post( + f"/api/recurring/{recurring_id}/complete", + json={"measurement_note": "45° Spagat gehalten"}, + headers=_auth(token), + ) + assert completed.status_code == 200 + body = completed.json() + assert body["completed_instance"]["status"] == "completed" + assert body["completed_instance"]["measurement_note"] == "45° Spagat gehalten" + assert body["next_instance"]["status"] == "open" + assert body["recurring"]["next_due_at"] + + refreshed = client.get( + f"/api/initiatives/{initiative_id}/recurring", + headers=_auth(token), + ).json() + assert refreshed[0]["open_cadence_instance"]["id"] == body["next_instance"]["id"] + + +def test_steering_next_action_includes_recurring_id(client): + user = provision_user_in_tenant(tenant_role="admin") + token = _login(client, user) + + created = _create_initiative( + client, + token, + title="Spagat Next", + archetype_key="initiative.maturity_journey", + ) + initiative_id = created.json()["id"] + + snap = client.get( + f"/api/initiatives/{initiative_id}/steering-snapshot", + headers=_auth(token), + ) + assert snap.status_code == 200 + next_actions = snap.json().get("next_actions") or [] + recurring_items = [a for a in next_actions if a.get("kind") == "recurring_due"] + assert recurring_items + assert recurring_items[0].get("recurring_id") diff --git a/backend/tests/test_ap23a_operating_context.py b/backend/tests/test_ap23a_operating_context.py index fa873a4..a881c8e 100644 --- a/backend/tests/test_ap23a_operating_context.py +++ b/backend/tests/test_ap23a_operating_context.py @@ -82,6 +82,8 @@ def test_operating_context_maturity(client): body = res.json() assert body["method_key"] == "maturity_progression" assert any(s["key"] == "journey" for s in body["ui_profile"]["processSteps"]) + assert "inbox" not in (body["ui_profile"].get("planOutlineKeys") or []) + assert body["ui_profile"]["planOutlineKeys"] == ["profile", "gates", "work"] def test_operating_context_program(client): diff --git a/docs/product/Kairo_Implementation_Truth_Table_v0.1.md b/docs/product/Kairo_Implementation_Truth_Table_v0.1.md index 5d852e2..ecd75ad 100644 --- a/docs/product/Kairo_Implementation_Truth_Table_v0.1.md +++ b/docs/product/Kairo_Implementation_Truth_Table_v0.1.md @@ -176,7 +176,7 @@ Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden. | maturity / sequential / queue / recurring Strategien | ✓ | AP2.0d; pytest + Deploy | | Referenz-Ausprägungen (Kumite, Buch, Kairo) | ◐ | Code-Registry; Nutzer wählt bei Anlage | | Archetyp Starter-Kits | ◐ | AP2.2a | -| MVP-Abnahfe Stufe A validiert | ◐ | AP2.1: Report v0.3 + Assignment — PO-Validation auf Dev ausstehend | +| MVP-Abnahfe Stufe A validiert | ✗ | AP2.1: A1 PO No-Go; A2/B2b offen — Register §7 | | Operational Actor API | ✓ | AP1.7: `/api/operational/` + Service Tokens | | Actor Service Token | ✓ | AP1.7c; Tenant-scoped, capability-gebunden | | MCP produktiv | ✗ | nach AP2.1 Go | @@ -220,6 +220,15 @@ Details: `Kairo_Status_Review_and_Next_Steps_v0.1.md` §2.1 | Roadmap-Phasen veraltet | Roadmap v0.2 §2 aktualisiert 2026-07-11 | | Usability Recovery → InitiativeDetail | IA-first, Redirects AP1.9a | +| **F-A1-09** | Leitfrage (≤2 Min) | Prozess/Doku | — | offen | Report §3.3 | + +--- + +## 7. Validation-Findings (AP2.1) + +PO-Smoke-Ergebnisse und Architektur-Einordnung: +`docs/sprints/Sprint1_AP2_1_Validation_Findings_Register_v0.1.md` + --- ## Nächste erwartete Truth-Table-Änderungen diff --git a/docs/product/Kairo_MVP_Execution_Plan_v0.2.md b/docs/product/Kairo_MVP_Execution_Plan_v0.2.md index 21dc2e9..2b31363 100644 --- a/docs/product/Kairo_MVP_Execution_Plan_v0.2.md +++ b/docs/product/Kairo_MVP_Execution_Plan_v0.2.md @@ -149,6 +149,8 @@ Auth: Actor Service Token (Tenant-scoped). Kein Gate-Verify, kein Lifecycle-Over Go-Kriterium: MVP v0.3 §5 — pro Stufe-A-Archetyp Leitfrage ≤2 Min + Next Action begründet; Portfolio Cockpit-Attention (Kriterium 7). +**Findings-Register (PO-Smoke + Architektur-Review):** `docs/sprints/Sprint1_AP2_1_Validation_Findings_Register_v0.1.md` + --- ## 8. Dokumentenpriorität diff --git a/docs/product/archetypes/SPEC_A1_maturity_journey_v0.1.md b/docs/product/archetypes/SPEC_A1_maturity_journey_v0.1.md index 51cd13e..9710dd7 100644 --- a/docs/product/archetypes/SPEC_A1_maturity_journey_v0.1.md +++ b/docs/product/archetypes/SPEC_A1_maturity_journey_v0.1.md @@ -228,6 +228,11 @@ Cadence im Detail am RecurringElement. ## 11. Ist-Stand (Spec vs. Code) +**PO-Validation 2026-07-28:** Detaillierte Findings + Architektur-Urteil → +`docs/sprints/Sprint1_AP2_1_Validation_Findings_Register_v0.1.md` (F-A1-01…10) + +**PO-Produkturteil A1:** **No-Go** — Leitfrage nicht erfüllt; Tages-Tracking, Messgrößen, Gate-Fortschritt und IA fehlen für echten Einsatz. Architektur/Spec **nicht** falsch; **Minimal-Slice** (CadenceInstance, Work=Übung) vor Re-Abnahme nötig. + | Bereich | Spec | Code/Truth | Lücke | |---------|------|------------|-------| | Registry / Kit | A1 + Stufen | weitgehend AP2.0e/Kit | Multi-Übung/Alternanz | diff --git a/docs/sprints/Sprint1_AP2_1_MVP_Validation_Report_v0.3.md b/docs/sprints/Sprint1_AP2_1_MVP_Validation_Report_v0.3.md index 36753cc..67f32f8 100644 --- a/docs/sprints/Sprint1_AP2_1_MVP_Validation_Report_v0.3.md +++ b/docs/sprints/Sprint1_AP2_1_MVP_Validation_Report_v0.3.md @@ -4,28 +4,33 @@ **Status:** Entwurf — vom Product Owner auszufüllen **Stand:** 2026-07-28 **Bezug:** `Kairo_MVP_Definition_v0.3.md` §5–8 -**Assignment:** `Sprint1_AP2_1_MVP_Validation_Assignment_v0.1.md` +**Assignment:** `Sprint1_AP2_1_MVP_Validation_Assignment_v0.1.md` +**Findings (Architektur-Review):** `Sprint1_AP2_1_Validation_Findings_Register_v0.1.md` | Feld | Wert | |------|------| -| **Kairo-Version / Deploy** | | +| **Kairo-Version / Deploy** | `0.20.2-ap2.2d` (schema 025) | | **Dev-URL** | https://dev.kairo.jinkendo.de | -| **Branch / Commit** | `develop` @ | -| **Tester (PO)** | | -| **Testdatum** | | +| **Branch / Commit** | `develop` @ `0baa595` | +| **Tester (PO)** | Lars | +| **Testdatum** | 2026-07-28 | --- ## 1. Executive Summary -| Entscheidung | ☐ Go Stufe A · ☐ Bedingt · ☐ No-Go | +| Entscheidung | ☐ Go Stufe A · ☐ Bedingt · ☑ No-Go (bis A1 nachgezogen oder Scope-PO) | |--------------|-------------------------------------| **Kurzfazit (3–5 Sätze):** +Technisch: pytest grün, A1-Happy-Path (Anlage → Verify → Stufenwechsel) durchspielbar. +Produkt (PO): **A1 No-Go** — Leitfrage nicht beantwortbar; tagesbezogenes Tracking, Messgrößen und Gate-Fortschritt fehlen; extreme Unübersichtlichkeit; **Hürden > Nutzen**. A2/B2b noch offen. **Blocker (falls No-Go / Bedingt):** +- **A1:** CadenceInstance/Tages-„erledigt“, Work=Übung, Gate-Fortschritt, IA — siehe Findings Register §6.1 (F-A1-02…10) +- **Gesamt Stufe A:** MVP §4 verlangt A1 — Go erst nach A1 Minimum Slice **oder** PO-Scope-Entscheidung (A1 aus Stufe-A-Abnahfe) --- @@ -44,8 +49,22 @@ ## 3. Szenario A1 — Spagat können (Reifegrad) -**Initiative-ID / Titel:** -**Archetyp:** `initiative.maturity_journey` · **Methode:** `maturity_progression` +**Initiative-ID / Titel:** Spagat können (PO-Smoke 2026-07-28) +**Archetyp:** `initiative.maturity_journey` · **Methode:** `maturity_progression` +**Zwischenurteil:** ☐ Go · ☐ Bedingt · ☑ **No-Go (Produkt)** — [Findings Register](Sprint1_AP2_1_Validation_Findings_Register_v0.1.md) F-A1-01…10 + +### 3.0 PO-Feedback (2026-07-28) + +| Thema | Urteil | +|-------|--------| +| Anlage + Starter-Kit | ✓ technisch | +| Kontrolle (Stufe, Next Action) | ✗ kein Tages-/Gate-Fortschrittsbild | +| Ausführen (Anti-Todo-Wand) | ✗ Übung nicht ausführbar; AP-Verwirrung | +| Verify + Stufenwechsel | ✓ technisch; PO: kein Ersatz für laufendes Tracking | +| Journey / Rhythmen | ✗ unübersichtlich | +| Leitfrage ≤2 Min | **✗** — nicht sinnvoll beantwortbar | + +**PO-Zitat (Kern):** *Für tagesbezogenes Tracking, Messmethoden und aktive Gate-Einschätzung unbrauchbar; Hürden größer als Nutzen.* ### 3.1 Smoke-Schritte (Dev) @@ -74,9 +93,9 @@ ### 3.3 Leitfrage-Antwort (PO, ≤2 Min) -**Nächster Schritt:** -**Warum:** -**Stopuhr (Min):** +**Nächster Schritt:** *Nicht sinnvoll bestimmbar* — kein klarer Tages-Handlungspfad (Übung erledigen + Fortschritt sehen). +**Warum:** Ausführen zeigt AP statt Übung; kein Tracking; Gate-Fortschritt nicht steuerbar; UI unübersichtlich. +**Stopuhr:** >2 Min / abgebrochen — **MVP §5.6 nicht erfüllt** ### 3.4 Anti-Patterns (§7) @@ -85,8 +104,7 @@ | Flache Liste aller Übungs-Tasks als Hauptbild | ☐ ja ☐ nein | | | Recurring im Action-Backlog statt Rhythmus-Ansicht | ☐ ja ☐ nein | | -**Lücken / UX-Schmerz:** - +**Lücken / UX-Schmerz:** PO No-Go — Register F-A1-02…10; Minimum Slice §6.1 vor Re-Test. Kein Architektur-Neudesign — fehlender A1-Kern (CadenceInstance, Today=Übung, Gate-Fortschritt, IA). --- diff --git a/docs/sprints/Sprint1_AP2_1_Validation_Findings_Register_v0.1.md b/docs/sprints/Sprint1_AP2_1_Validation_Findings_Register_v0.1.md new file mode 100644 index 0000000..746a751 --- /dev/null +++ b/docs/sprints/Sprint1_AP2_1_Validation_Findings_Register_v0.1.md @@ -0,0 +1,229 @@ +# AP2.1 — Validation Findings Register +## v0.1 (PO-Smoke → Architektur-Review) + +**Status:** aktiv — wird während AP2.1 ergänzt +**Stand:** 2026-07-28 (A1 Minimum Slice implementiert — PO Re-Abnahme ausstehend) +**Bezug:** `Sprint1_AP2_1_MVP_Validation_Report_v0.3.md`, `Kairo_MVP_Definition_v0.3.md` §5–8 +**Dev:** https://dev.kairo.jinkendo.de · nach Deploy Migration **030** + A1-Slice + +--- + +## 1. Zweck + +Dieses Register hält **PO-Beobachtungen aus der MVP-Validation** fest und ordnet sie architektonisch ein: + +| Kategorie | Bedeutung | +|-----------|-----------| +| **Bug** | Abweichung vom **aktuellen** Soll (Spec + gelieferte Architektur) — Fix ohne ADP | +| **Config-Drift** | Inkonsistenz innerhalb der Plugin-/Profile-Schicht (kein Modellbruch) | +| **Implementierungsschuld** | Architektur und Spec stimmen; Code liefert Teilmenge | +| **Aufgeschoben (Spec)** | Explizit in Archetyp-/Methoden-Spec oder ADP als „später“ / Stufe B+ | +| **Architektur-Lücke** | Zielmodell dokumentiert, tragfähige Basis fehlt oder widerspricht sich **über** Spec hinaus | + +**Leitfrage des Reviews:** Brauchen wir ein **neues Architektur-Design**, oder ist die Plugin-/Kernel-Schicht korrekt und es fehlen **Slices**? + +--- + +## 2. Architektur-Urteil (Stand nach A1-Smoke) + +### 2.1 Kurzfazit (technisch) + +**Kein generelles Architektur-Design-Problem.** Die vier Schichten (Methode → Archetyp → Profil → UI-Composition, AP2.3/AP2.4) tragen den A1-Happy-Path **technisch**. + +### 2.1b PO-Produkturteil A1 (2026-07-28) + +**No-Go für echten Einsatz** — unabhängig vom grünen E2E-Test. + +> Leitfrage lässt sich nicht sinnvoll beantworten. Für tagesbezogenes Tracking (durchgeführt; optional Messwerte wie Anzahl, 45°-Spagat), aktive Einschätzung des Gate-Fortschritts und übersichtliche Steuerung ist der Vorhabentyp **unbrauchbar**. Hürden > Nutzen. + +| Dimension | Technische Validation | PO-Produktnutzen | +|-----------|----------------------|------------------| +| Happy Path (Anlage → Verify) | ✓ | ◐ — nur Demo, kein Alltag | +| Leitfrage ≤2 Min (MVP §5.6) | — | **✗** | +| Tages-Tracking / Übung erledigen | ✗ | **Blocker** | +| Messgrößen / Metriken | ✗ | **Blocker** (PO) | +| Gate-Fortschritt sichtbar steuerbar | ◐ | **✗** | +| Übersichtlichkeit | ◐ | **✗** | + +**Architektur-Einordnung:** PO-Anforderungen stehen **bereits in SPEC A1 / Spec-D** (CadenceInstance, Tages-Übung, Kriterien, Metriken). Es fehlt nicht ein neues Design — es fehlt der **Minimal-Slice für nutzbaren A1**, der fälschlich als „post-MVP“ behandelt wurde. + +**Konsequenz AP2.1 / MVP Stufe A:** A1 erfüllt **nicht** die MVP-Nutzbarkeits-Bar §5 für PO-Abnahme. Gesamt-Go Stufe A **blockiert**, solange A1 Pflicht-Szenario bleibt (`Kairo_MVP_Definition_v0.3.md` §4). + +Die Validation zeigt drei **Lückentypen**: + +```text +(1) Config-Drift planOutlineKeys ≠ dataSlices → schneller Fix +(2) Spec-Deferred CadenceInstance, Alternanz, metric → geplant, Spec §11 +(3) Polymorphie-Gap Kernel Q2 Work-Items: BE ja, FE nein → Implementierungsschuld +``` + +### 2.2 Referenz-Architektur (Prüfmaßstab) + +| Dokument | Rolle | +|----------|--------| +| `ADP_Archetype_Method_Plugin_Architecture_v0.1.md` | Route Gating, Slices, kein Page-If | +| `SPEC_A1_maturity_journey_v0.1.md` §4, §8, §11 | A1-Fachmodell + Ist-Stand-Tabelle | +| `methods/SPEC_D_maturity_progression_v0.1.md` D4–D6 | Leading = CadenceInstance; Ranking | +| `ADP_Roadmap_Graph_and_Gate_Checklist_v0.1.md` | Kriterien, Evidence, `metric` später | +| `Kairo_Steering_Method_Kernel_v0.1.md` Q2 | Work-Item-Polymorphie | +| `Kairo_Steering_Method_Normalization_Program_v0.1.md` | CadenceInstance als Normalform | + +### 2.3 Was **nicht** neu designt werden muss + +- Archetyp ↔ Methode ↔ UI-Profil (Operating Context) +- Gate-Verify + Kriterien-Checkliste (AP1.4b) +- Stufenwechsel + Recurring-Rotation (AP2.0e) +- Composition-Slots (`maturity_stage`, `recurring_rhythm`, `work.today`) + +--- + +## 3. Findings — Szenario A1 (Spagat / Reifegrad) + +**PO-Validation:** 2026-07-28 · Vorhaben „Spagat können“ · `initiative.maturity_journey` +**Urteil A1:** **No-Go (Produkt)** · technischer Kernpfad ✓ · MVP §5 für PO **✗** + +| ID | PO-Beobachtung | Kategorie | Architektur-Urteil | MVP Stufe A | Follow-up | +|----|----------------|-----------|-------------------|-------------|-----------| +| **F-A1-01** | Klick **Plan → Eingang** leitet auf **Zielzustände** um | **Config-Drift** | `planOutlineKeys` enthält `inbox`, `dataSlices` ohne `backlog` → Route Gating korrekt, Nav falsch. **Kein Modellproblem.** Spec A1 §8: Eingang „nicht dominant“. | Mitverantwortlich Unübersicht | **Fix:** `inbox` aus `planOutlineKeys` bei `initiative.maturity_journey` | +| **F-A1-02** | Keine Konfiguration **Wochentage / Rhythmus**; kein **Tages-Tracking** („heute erledigt“) | **Implementierungsschuld** (Spec-Pflicht) | Spec A1 §4.3–4.5 + Spec-D D4: `CadenceInstance`. Code: nur `RecurringElement` + Fälligkeit. **Kein Designbruch — fehlender Kern-Slice.** | **PO-Blocker** | **AP A1-Cadence-MVP:** Instanz + „heute erledigt“ | +| **F-A1-03** | **Ausführen:** AP statt Übung; Rhythmus-Link tot; AP nicht öffenbar | **Implementierungsschuld** | Kernel Q2: Leading = CadenceInstance; FE verlinkt nur Actions. | **PO-Blocker** | **AP A1-Work-UI** | +| **F-A1-04** | Kriterien auto-erfüllt via Evidence; keine laufende Gate-Einschätzung | **By Design** + **Lücke UX** | AP1.4b korrekt für Verify-Moment; PO braucht **Fortschrittsbild** während Stufe (Spec A1 §8 Kontrolle). | **PO-Blocker** (Einschätzung) | Fortschritts-Panel + manuelle/metric Kriterien | +| **F-A1-05** | Keine Messung (4×/Woche, 80 Punkte, 45° Spagat, …) | **Implementierungsschuld** (Spec vorbereitet) | `criterion_kind: metric` + Cadence-Erfüllung geplant; keine Engine. Schema reicht — **Feature fehlt, kein ADP neu.** | **PO-Blocker** | Messwert am Tagesabschluss + optional metric-Kriterium | +| **F-A1-06** | Keine **kontinuierliche** Fortschrittskontrolle in der Stufe | **Implementierungsschuld** | Plan/Ist-Trennung korrekt; Ist-Tracking für Übungen fehlt (CadenceInstance). | **PO-Blocker** | Teil A1-Cadence-MVP | +| **F-A1-07** | Nach Verify keine AP-Aktivierung; nur Recurring rotiert | **Implementierungsschuld** | Spec A1 §4.4 Activity Set — Scope AP2.0e minimal. | Sekundär vs. F-A1-02–06 | AP A1-Activity-Set | +| **F-A1-08** | Journey unklar; pausierte Übung verwirrend | **IA** | Technisch korrekt (AP2.0e); PO wahrgenommene Unübersicht. | Mitverantwortlich | IA A1 Today-first | +| **F-A1-09** | **Leitfrage** nicht beantwortbar — Hürden > Nutzen | **PO-Produkturteil** | MVP §5.6 nicht erfüllt. Symptom von F-A1-02…05, nicht Doku-Problem. | **No-Go** | siehe §8 Minimum-Slice | +| **F-A1-10** | **Extreme Unübersichtlichkeit** (Eingang-Redirect, AP vs. Übung, Journey vs. Rhythmen) | **IA + Implementierungsschuld** | Plugin-Architektur OK; **A1-Profil und Work-Default** spiegeln Nutzerbild Spec §8 nicht. Kein neues OM nötig — **falsche/fehlende UI-Hülle**. | **PO-Blocker** | A1-IA-Slice (Eingang weg, Today=Übung, klare Stufenleiste) | + +--- + +## 4. Detail-Notizen (Architektur-Check) + +### F-A1-01 — Eingang vs. Slices + +```text +ui_profiles.py initiative.maturity_journey: + planOutlineKeys: [profile, gates, inbox, work] ← Nav zeigt Eingang + dataSlices: [actions, roadmap, recurring, …] ← kein "backlog" + +viewRegistry.js /plan/inbox requiredSlices: ['backlog'] +→ isRouteAllowedForMode = false → Redirect planDefaultRoute (/plan/gates) +``` + +**Urteil:** Plugin-Architektur funktioniert wie designed; **Profil-Seed ist inkonsistent**. + +### F-A1-02 / F-A1-06 — CadenceInstance-Schicht + +| Schicht | Spec | Code heute | +|---------|------|------------| +| RecurringElement | Container + Bindung Stufe | ✓ | +| Cadence / Wochentage / Alternanz | Spec A1 §4.3–4.5 | ✗ | +| CadenceInstance (eine offene Instanz) | Spec-D D4, Kernel Q2 | ✗ (nur `next_due_at` am Element) | +| Erfüllung → nächste Instanz | Spec A1 Happy Path §9.3 | ◐ (manuell / overdue-Logik minimal) | + +**Urteil:** **Kein falsches Design** — Normalization-Programm beschreibt Ziel; MVP lieferte **Minimal-Rhythmus**. Spec A1 §11 listet das explizit als Lücke. + +### F-A1-03 — Work-Item-Polymorphie (Kernel Q2) + +| Work Item | Next Action BE | Next Action FE Link | +|-----------|----------------|---------------------| +| Action | ✓ | ✓ | +| Backlog | ✓ | ✓ | +| Recurring / Cadence | ✓ (`recurring_due`) | ✗ (nur Text `recommended_action`) | + +**Urteil:** **Architektur-Lücke nur in der Präsentationsschicht** — Steering-Kernel und Strategies sind erweiterbar; FE muss polymorphe Ziele unterstützen (Link-Typ-Registry analog Backend `kind`). + +### F-A1-04 / F-A1-05 — Kriterien & Metriken + +- **Evidence → satisfied:** beabsichtigt (`roadmap_criteria.prepare_criteria_for_verify`). +- **Manuelle Kriterien:** bleiben offen bis manuell / waived — korrekt. +- **Metriken:** Schema vorbereitet, **keine** Regel-Engine — ADP „später“. + +**Urteil:** Gate-Modell ist **generisch genug**; fehlende Metrik-Auswertung ist **Feature-Deferral**, kein Redesign. + +### F-A1-07 — Activity Set bei Stufenwechsel + +`maturity_stage_transition.on_maturity_stage_reached`: + +- pausiert aktive Recurring ✓ +- aktiviert nächste Stufe ✓ +- legt neue Recurring-Übung an ✓ +- **keine** Stage-Actions, **keine** Graph-Aktivierung weiterer Plan-APs + +Spec A1 §4.4 verlangt volles Set — **Implementierungsschuld**, Scope AP2.0e war Minimal-Slice. + +--- + +## 5. Abgleich mit SPEC_A1 §11 (Ist-Stand-Tabelle) + +| Spec §11 Zeile | Validation bestätigt | +|----------------|-------------------| +| Multi-Übung / Alternanz fehlt | F-A1-02 ✓ | +| Stage Activity Set Teilmenge | F-A1-07 ✓ | +| UI Stufe-Detail lückenhaft | F-A1-02, F-A1-03 ✓ | +| steering_elements FE/BE Parität | F-A1-03 (recurring in Kontrolle ✓, Work ◐) | + +**Keine neue Spec-Zeile nötig** — Register präzisiert PO-Sicht für AP2.1. + +**Urteil:** **Kein falsches Design** — PO-Anforderung = Spec §4 / Spec-D. MVP lieferte **Infrastruktur ohne Ist-Schicht**. Für PO-Abnahme ist das **kein „später“**, sondern **fehlender A1-Kern**. + +### F-A1-09 / F-A1-10 — PO-Leitfrage & Unübersichtlichkeit + +**PO (2026-07-28, wörtlich zusammengefasst):** + +- Leitfrage nicht sinnvoll beantwortbar +- Tages-Tracking + optional Messwerte (Anzahl, Grad, …) + aktive Gate-Einschätzung **erforderlich** für echten Einsatz +- Extreme Unübersichtlichkeit → **Hürden größer als Nutzen** + +**Architektur:** Symptome von fehlendem CadenceInstance-Modell, Work-UI auf AP statt Übung, IA-Rauschen (Eingang, Journey). **Kein Widerspruch** zwischen Vision und Spec — **Widerspruch zwischen geliefertem MVP-Slice und PO-Mindestnutzen**. + +--- + +## 6. Priorisierte Follow-ups (PO-gesteuert) + +### 6.1 A1 Minimum Usability Slice (vor erneuter A1-Abnahme) + +**Implementiert (2026-07-28, uncommitted):** Migration 030, `cadence_instance` + `POST /api/recurring/{id}/complete`, `WorkTodayPracticePanel`, Next-Action-Link `#today-practice`, Gate-Fortschritt in `MaturityStagePanel`, Eingang aus A1-Nav. **Offen:** PO Re-Abnahme; Metric-Kriterium manuell (Slice #5, optional vor Re-Test). + +| # | Lieferung | Decke PO-Schmerz | Status | +|---|-----------|------------------|--------| +| 1 | **CadenceInstance + „Heute erledigt“** | Tages-Tracking | ✓ implementiert | +| 2 | **Ausführen/Today = Übung** | Nutzen Alltag | ✓ implementiert | +| 3 | **Kontrolle: Gate-Fortschritt** | Gate-Einschätzung | ✓ implementiert (Kriterien offen/erfüllt) | +| 4 | **IA A1** (Eingang weg) | Unübersichtlichkeit | ✓ implementiert | +| 5 | **Metric-Kriterium manuell** | Messgrößen | ◐ Messnotiz am Complete; Auto-Engine weiter offen | + +Alternanz, volle Metric-Engine, Activity-Set-Automation → **nach** Slice 1–4. + +### 6.2 Technische Quick-Wins (P0) + +| Prio | ID | Slice | +|------|-----|-------| +| P0 | F-A1-01 | Eingang aus A1-Nav | +| P1 | F-A1-03 | Next Action → Recurring/Übung-Aktion | + +--- + +## 7. Konsequenz für AP2.1 / MVP Stufe A + +| Option | Bedeutung | +|--------|-----------| +| **A — A1 nachziehen** | Validation pausieren; **A1 Minimum Slice** (§6.1) liefern; A1 erneut smoke-testen | +| **B — MVP-Definition anpassen** | A1 vorübergehend aus Stufe-A-Abnahfe (PO-Entscheidung + Doku) — **nur wenn A2+B2b als MVP reichen** | +| **C — Gesamt No-Go** | AP2.1 stoppt bei Stufe A bis A1 oder Scope-Entscheidung | + +**Empfehlung:** **Option A** — Architektur trägt; Spec war richtig; Implementierung war zu dünn. A2/B2b-Smoke **parallel** möglich, Gesamt-Go aber nicht ohne A1-Lösung (Option A oder explizite B). + +--- + +## 8. Nächste Register-Einträge + +| Szenario | Status | +|----------|--------| +| A2 — Neue Küche (Linear) | ausstehend | +| B2b + B3 — Product + Sprint | ausstehend | +| Portfolio / Cockpit (Kriterium 7) | ausstehend | + +--- + +*Aktualisieren bei jedem PO-Smoke-Schritt. Bei Kategorie „Architektur-Lücke“ → ADP prüfen, nicht sofort implementieren.* diff --git a/docs/sprints/Sprint1_AP_A1_Minimum_Usability_Slice_v0.1.md b/docs/sprints/Sprint1_AP_A1_Minimum_Usability_Slice_v0.1.md new file mode 100644 index 0000000..f761430 --- /dev/null +++ b/docs/sprints/Sprint1_AP_A1_Minimum_Usability_Slice_v0.1.md @@ -0,0 +1,61 @@ +# AP A1 — Minimum Usability Slice +## Auftrag v0.1 + +**Status:** freigegeben (PO folgt Option A, 2026-07-28) · **Implementierung A1.1–A1.3 abgeschlossen** (Re-Abnahme ausstehend) +**Auslöser:** AP2.1 PO No-Go A1 — [`Sprint1_AP2_1_Validation_Findings_Register_v0.1.md`](Sprint1_AP2_1_Validation_Findings_Register_v0.1.md) §6.1 +**Ziel:** A1 **real nutzbar** für Tages-Tracking + Gate-Einschätzung + klare Today-Ansicht — **ohne** Architektur-Neudesign. + +--- + +## 1. Nicht-Ziele (dieser Slice) + +- Alternanz-Gruppen, Wochentags-Editor vollständig +- Automatische Metric-Engine (4×/Woche auto-count) +- Volles Activity Set bei Stufenwechsel (Stage-APs auto-aktivieren) +- A3/Care CadenceInstance-Parität + +--- + +## 2. Lieferumfang (Pflicht für Re-Abnahme A1) + +| # | Lieferung | Findings | Akzeptanz | +|---|-----------|----------|-----------| +| 1 | **`cadence_instances`** + API `complete` | F-A1-02, F-A1-06 | Eine offene Instanz pro aktivem Recurring; „Heute erledigt“ → nächste Fälligkeit | +| 2 | **`WorkTodayPracticePanel`** | F-A1-03 | Ausführen/Today zeigt Übung zuerst; optional Messnotiz | +| 3 | **Next Action → Übung** | F-A1-03 | Klickbarer Pfad zu Today-Übung | +| 4 | **Gate-Fortschritt** am aktiven Stufe-Panel | F-A1-04 | Kriterien offen/erfüllt sichtbar + Link Gate-Detail | +| 5 | **IA A1** | F-A1-01, F-A1-10 | Eingang aus Nav; Plan-Outline nur profile + gates + work | + +--- + +## 3. Architektur-Bezug (kein ADP nötig) + +| Baustein | Dokument | +|----------|----------| +| CadenceInstance | `Kairo_Steering_Method_Normalization_Program_v0.1.md`, Spec-D `maturity_progression` D4 | +| Work-Item-Polymorphie FE | Kernel Q2 — Link-Typ `recurring_id` | +| Gate-Kriterien | AP1.4b — bestehend | + +--- + +## 4. Pakete + +```text +A1.1 Migration 030 + cadence_instance service + complete API + pytest +A1.2 WorkTodayPracticePanel + Composition + NextAction-Link +A1.3 MaturityStagePanel Gate-Fortschritt + ui_profile IA +A1.4 Re-Validation A1 (PO) → Report §3 Go/Bedingt +``` + +--- + +## 5. Re-Abnahme-Kriterien (PO) + +1. PO kann **heute** Übung als erledigt markieren (+ optional Messnotiz). +2. **Leitfrage ≤2 Min:** „Welche Übung heute?“ — ohne AP-Detour. +3. **Kontrolle:** aktive Stufe + Kriterien-Fortschritt erkennbar. +4. Kein Eingang-Redirect / Nav-Rauschen. + +--- + +*Nach Abschluss: Findings Register F-A1-* auf erledigt setzen; AP2.1 A1 erneut smoke-testen.* diff --git a/frontend/src/api/recurring.js b/frontend/src/api/recurring.js index a814109..2112d35 100644 --- a/frontend/src/api/recurring.js +++ b/frontend/src/api/recurring.js @@ -21,3 +21,10 @@ export function updateRecurring(recurringId, body) { export function deleteRecurring(recurringId) { return apiFetch(`/api/recurring/${recurringId}`, { method: 'DELETE' }) } + +export function completeRecurringPractice(recurringId, body = {}) { + return apiFetch(`/api/recurring/${recurringId}/complete`, { + method: 'POST', + body: JSON.stringify(body), + }) +} diff --git a/frontend/src/components/MaturityStagePanel.jsx b/frontend/src/components/MaturityStagePanel.jsx index 97a1a34..3bc6fe9 100644 --- a/frontend/src/components/MaturityStagePanel.jsx +++ b/frontend/src/components/MaturityStagePanel.jsx @@ -1,28 +1,66 @@ +import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { MILESTONE_STATUS_LABELS } from '../constants/status.js' +import { listInitiativeRoadmapCriteriaProgress } from '../api/roadmap.js' +import { gatePath, scopedPath } from '../utils/routes.js' import { EmptyState } from './EmptyState.jsx' -import { scopedPath } from '../utils/routes.js' export function MaturityStagePanel({ initiativeId, roadmapItems = [], embedded = true, }) { + const [criteriaProgress, setCriteriaProgress] = useState({}) + const stages = [...roadmapItems] .filter((item) => item.item_type === 'maturity_stage') .sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) const activeStage = stages.find((s) => s.status === 'active') + useEffect(() => { + if (!initiativeId) return undefined + let cancelled = false + listInitiativeRoadmapCriteriaProgress(initiativeId) + .then((data) => { + if (!cancelled) setCriteriaProgress(data || {}) + }) + .catch(() => { + if (!cancelled) setCriteriaProgress({}) + }) + return () => { + cancelled = true + } + }, [initiativeId, roadmapItems.length]) + + const activeProgress = activeStage ? criteriaProgress[activeStage.id] : null + const body = stages.length === 0 ? ( ) : ( <> {activeStage && ( -

- Aktive Stufe: {activeStage.title} -

+ <> +

+ Aktive Stufe:{' '} + + {activeStage.title} + +

+ {activeProgress && ( +

+ Gate-Fortschritt:{' '} + + {activeProgress.closed ?? 0} / {activeProgress.total ?? 0} + {' '} + Kriterien erfüllt + {(activeProgress.open ?? 0) > 0 && ( + · {activeProgress.open} offen + )} +

+ )} + )}
    {stages.map((stage, index) => ( @@ -36,7 +74,9 @@ export function MaturityStagePanel({ > {index + 1}
    - {stage.title} + + {stage.title} + {MILESTONE_STATUS_LABELS[stage.status] || stage.status} @@ -45,9 +85,13 @@ export function MaturityStagePanel({ ))}

- Stufen und Übungen unter{' '} - - Kontrolle → Rhythmen & Journey + Kriterien und Verify am{' '} + + Plan → Stufen + + ; Übung unter{' '} + + Ausführen → Heute .

@@ -62,7 +106,7 @@ export function MaturityStagePanel({

Reifegrad-Stufen

- Aktive Stufe und Fortschritt — Steuerungshorizont für Reifegrad-Vorhaben (A1). + Aktive Stufe, Gate-Fortschritt und Orientierung — Steuerungshorizont (A1).

diff --git a/frontend/src/components/WorkTodayPracticePanel.jsx b/frontend/src/components/WorkTodayPracticePanel.jsx new file mode 100644 index 0000000..56900ad --- /dev/null +++ b/frontend/src/components/WorkTodayPracticePanel.jsx @@ -0,0 +1,99 @@ +import { useMemo, useState } from 'react' +import { completeRecurringPractice } from '../api/recurring.js' +import { EmptyState } from './EmptyState.jsx' + +function formatDue(iso) { + if (!iso) return null + try { + return new Date(iso).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' }) + } catch { + return iso + } +} + +/** + * A1 — Heutige Übung (CadenceInstance): Tages-Tracking mit optionaler Messnotiz. + */ +export function WorkTodayPracticePanel({ + initiativeId, + recurringItems = [], + canManage = false, + onCompleted, + busy = false, +}) { + const [measurement, setMeasurement] = useState('') + const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState(null) + + const activePractice = useMemo(() => { + return (recurringItems || []).find((item) => item.status === 'active') + }, [recurringItems]) + + const openInstance = activePractice?.open_cadence_instance + + if (!activePractice) { + return ( +
+ +
+ ) + } + + async function handleComplete(e) { + e.preventDefault() + if (!canManage || submitting) return + setSubmitting(true) + setError(null) + try { + await completeRecurringPractice(activePractice.id, { + measurement_note: measurement.trim() || undefined, + }) + setMeasurement('') + await onCompleted?.() + } catch (err) { + setError(err.message) + } finally { + setSubmitting(false) + } + } + + return ( +
+
+
+

Heutige Übung

+

+ Tages-Tracking — erledigt markieren, optional Messwert notieren (z. B. Grad, Wiederholungen). +

+
+
+ +
+ {activePractice.title} + {openInstance?.due_at && ( +

Fällig: {formatDue(openInstance.due_at)}

+ )} +
+ + {canManage ? ( +
+ + {error &&

{error}

} + +
+ ) : ( +

Keine Berechtigung zum Markieren der Übung.

+ )} +
+ ) +} diff --git a/frontend/src/composition/InitiativeCompositionSurface.jsx b/frontend/src/composition/InitiativeCompositionSurface.jsx index d3c314f..ebde374 100644 --- a/frontend/src/composition/InitiativeCompositionSurface.jsx +++ b/frontend/src/composition/InitiativeCompositionSurface.jsx @@ -37,6 +37,7 @@ export function InitiativeCompositionSurface({ uiFeatures: ops.uiFeatures, dataSlices: ops.dataSlices, onAcceptBacklogProposal, + refreshOm: ops.refreshOm, }), [ ops.initiativeId, @@ -53,6 +54,7 @@ export function InitiativeCompositionSurface({ ops.uiFeatures, ops.dataSlices, onAcceptBacklogProposal, + ops.refreshOm, ], ) diff --git a/frontend/src/composition/compositionProviders.js b/frontend/src/composition/compositionProviders.js index 59c70ce..fdfaa0b 100644 --- a/frontend/src/composition/compositionProviders.js +++ b/frontend/src/composition/compositionProviders.js @@ -78,6 +78,16 @@ export const COMPOSITION_PROVIDERS = [ scopeTypes: ['initiative'], order: 16, }, + { + key: 'work.practice.today', + kind: 'steering_element', + steeringElement: 'recurring_rhythm', + slotKeys: ['work.today.primary'], + componentKey: 'WorkTodayPracticePanel', + requiresCapability: 'kairo.recurring.read', + scopeTypes: ['initiative'], + order: 5, + }, { key: 'steering.next_action', kind: 'steering_element', diff --git a/frontend/src/composition/providerComponents.jsx b/frontend/src/composition/providerComponents.jsx index 7cb9107..74e4bdb 100644 --- a/frontend/src/composition/providerComponents.jsx +++ b/frontend/src/composition/providerComponents.jsx @@ -7,6 +7,7 @@ import { RecurringRhythmPanel } from '../components/RecurringRhythmPanel.jsx' import { AgentSlotsPanel } from '../components/AgentSlotsPanel.jsx' import { SteeringProposalsPanel } from '../components/SteeringProposalsPanel.jsx' import { NextActionWidget } from '../widgets/NextActionWidget.jsx' +import { WorkTodayPracticePanel } from '../components/WorkTodayPracticePanel.jsx' import { WorkActionsPanel } from './WorkActionsPanel.jsx' /** Roadblocker-Strip — extrahiert aus InitiativeOverviewPage für Composition. */ @@ -45,6 +46,7 @@ export const PROVIDER_COMPONENTS = { RoadblockersStrip, WidgetHost, WorkActionsPanel, + WorkTodayPracticePanel, } /** diff --git a/frontend/src/composition/resolveSteeringComposition.js b/frontend/src/composition/resolveSteeringComposition.js index c043d37..da6ab65 100644 --- a/frontend/src/composition/resolveSteeringComposition.js +++ b/frontend/src/composition/resolveSteeringComposition.js @@ -201,6 +201,18 @@ export function buildProviderProps(provider, input) { initiativeId: ops.initiativeId, recurringItems: ops.recurringItems || [], } + case 'WorkTodayPracticePanel': + return { + initiativeId: ops.initiativeId, + recurringItems: ops.recurringItems || [], + canManage: capabilities.has('kairo.recurring.manage'), + busy: ops.formBusy, + onCompleted: async () => { + if (typeof ops.refreshOm === 'function') { + await ops.refreshOm(['recurring', 'steering_snapshot']) + } + }, + } case 'NextActionWidget': { const nextActionUi = resolveNextActionUi(elements, input.steeringSnapshot) return { diff --git a/frontend/src/composition/resolveSteeringComposition.test.js b/frontend/src/composition/resolveSteeringComposition.test.js index a6e82df..d6ea736 100644 --- a/frontend/src/composition/resolveSteeringComposition.test.js +++ b/frontend/src/composition/resolveSteeringComposition.test.js @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { COMPOSITION_PROVIDERS } from './compositionProviders.js' import { resolveSteeringComposition, @@ -265,19 +265,39 @@ describe('resolveSteeringComposition', () => { const result = resolveSteeringComposition({ surfaceKey: 'work.today', scope: 'initiative', - capabilities: CAPS, - steeringElements: ['next_action_primary', 'critical_path'], + capabilities: new Set([...CAPS, 'kairo.recurring.read']), + steeringElements: ['next_action_primary', 'recurring_rhythm', 'critical_path'], steeringSnapshot: { counts: {}, next_actions: [{ kind: 'action', title: 'X' }] }, opsContext: { initiativeId: 'init-1' }, }) expect(result.slots['work.today.primary']?.some((p) => p.key === 'steering.next_action')).toBe( true, ) + expect(result.slots['work.today.primary']?.some((p) => p.key === 'work.practice.today')).toBe( + true, + ) expect(result.slots['work.today.actions']?.some((p) => p.key === 'work.actions.today')).toBe( true, ) }) + it('buildProviderProps wires WorkTodayPracticePanel complete refresh', async () => { + const provider = COMPOSITION_PROVIDERS.find((p) => p.key === 'work.practice.today') + const refreshOm = vi.fn().mockResolvedValue(undefined) + const props = buildProviderProps(provider, { + opsContext: { + initiativeId: 'init-1', + recurringItems: [{ id: 'rec-1', status: 'active', title: 'Spagat' }], + formBusy: false, + refreshOm, + }, + capabilities: new Set([...CAPS, 'kairo.recurring.read', 'kairo.recurring.manage']), + }) + expect(props.canManage).toBe(true) + await props.onCompleted() + expect(refreshOm).toHaveBeenCalledWith(['recurring', 'steering_snapshot']) + }) + it('activates sprint backlog provider when active work cycle', () => { const result = resolveSteeringComposition({ surfaceKey: 'work.sprint', diff --git a/frontend/src/config/methodUiDefaults.js b/frontend/src/config/methodUiDefaults.js index 2b3ab78..70c334b 100644 --- a/frontend/src/config/methodUiDefaults.js +++ b/frontend/src/config/methodUiDefaults.js @@ -70,7 +70,7 @@ const ARCHETYPE_UI = { planDefaultRoute: '/plan/gates', workDefaultRoute: '/work/today', controlDefaultRoute: '/control/status', - planOutlineKeys: ['profile', 'gates', 'inbox', 'work'], + planOutlineKeys: ['profile', 'gates', 'work'], workNavKeys: ['today', 'mine', 'sprint'], }, 'initiative.program': { diff --git a/frontend/src/config/methodUiDefaults.test.js b/frontend/src/config/methodUiDefaults.test.js index 1ddfe0d..cc1b3e5 100644 --- a/frontend/src/config/methodUiDefaults.test.js +++ b/frontend/src/config/methodUiDefaults.test.js @@ -64,6 +64,17 @@ describe('methodUiDefaults', () => { expect(resolvePlanOutlineNodes({}).length).toBe(6) }) + it('maturity archetype omits inbox from plan outline', () => { + const ui = resolveMethodUiDefaults({ archetypeKey: 'initiative.maturity_journey' }) + expect(ui.planDefaultRoute).toBe('/plan/gates') + expect(ui.planOutlineKeys).toEqual(['profile', 'gates', 'work']) + expect(resolvePlanOutlineNodes({ archetypeKey: 'initiative.maturity_journey' }).map((n) => n.key)).toEqual([ + 'profile', + 'gates', + 'work', + ]) + }) + it('resolves active process step from pathname', () => { const steps = resolveProcessSteps({ archetypeKey: 'initiative.product' }) expect(resolveActiveProcessStepKey('/plan/inbox', steps)).toBe('inbox') diff --git a/frontend/src/widgets/NextActionWidget.jsx b/frontend/src/widgets/NextActionWidget.jsx index 3f582e7..2b511f2 100644 --- a/frontend/src/widgets/NextActionWidget.jsx +++ b/frontend/src/widgets/NextActionWidget.jsx @@ -9,7 +9,11 @@ import { WidgetCard } from '../components/WidgetCard.jsx' import { actionPath, scopedPath } from '../utils/routes.js' import { formatPortfolioRank } from '../utils/portfolioRank.js' +import { useProgramScope } from '../context/ProgramScopeContext.jsx' + function NextActionList({ items, initiativeId, showInitiativeLink = true }) { + const { hrefWithScope } = useProgramScope() + if (items.length === 0) { return ( @@ -17,6 +21,12 @@ function NextActionList({ items, initiativeId, showInitiativeLink = true }) { } function actionLinkForItem(item) { + if (item.recurring_id && item.initiative_id) { + return { + to: `${hrefWithScope('/work/today', { initiativeId: item.initiative_id })}#today-practice`, + label: 'Übung ausführen', + } + } if (item.action_id) { return { to: actionPath(item.action_id), label: 'Arbeitspaket' } }