diff --git a/README.md b/README.md index fbac34a..3ea21bb 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,18 @@ Operating Model Integration (AP0.10, Schema weiter `008`): Version nach AP0.10: **`0.10.0-ap0.10`**. Brücke zum Steering Core: `docs/architecture/Kairo_Operating_Model_Steering_Bridge_v0.1.md` +Operating Model Integration UI (AP0.10b, Schema weiter `008`): + +| Endpoint | Methode | Capability | Beschreibung | +|----------|---------|------------|--------------| +| `/api/workspace/actions/today` | GET | `kairo.workspace.read` | Meine Maßnahmen heute (überfällig zuerst) | + +**Workspace-Widgets:** Nächste sinnvolle Schritte, Heute, Aufmerksamkeit (erweitert) + +**Steuerungszustand v2:** Meilenstein-Horizont, Next Actions pro Vorhaben, Phase-Klartext + +Version nach AP0.10b: **`0.10.1-ap0.10b`** + Tenant-Invarianten: `docs/architecture/Kairo_Tenant_Invariants_v0.1.md` `docs/architecture/Kairo_Tenant_Invariants_v0.1.md` diff --git a/backend/data_layer/actions.py b/backend/data_layer/actions.py index bb88833..bf0b031 100644 --- a/backend/data_layer/actions.py +++ b/backend/data_layer/actions.py @@ -22,15 +22,37 @@ def _list_assigned_actions( tenant_id: str, actor_id: str, statuses: list[str], + order_today: bool = False, ) -> list[dict[str, Any]]: conn = get_connection() + order_clause = """ + ORDER BY + CASE WHEN a.due_at IS NOT NULL AND a.due_at < NOW() THEN 0 ELSE 1 END, + CASE a.status WHEN 'blocked' THEN 0 WHEN 'review_required' THEN 1 ELSE 2 END, + CASE a.priority + WHEN 'high' THEN 0 + WHEN 'normal' THEN 1 + WHEN 'low' THEN 2 + END, + a.due_at ASC NULLS LAST, + a.updated_at DESC + """ if order_today else """ + ORDER BY + CASE a.priority + WHEN 'high' THEN 0 + WHEN 'normal' THEN 1 + WHEN 'low' THEN 2 + END, + a.updated_at DESC + """ try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( - """ + f""" SELECT a.id, a.tenant_id, a.initiative_id, a.title, a.description, - a.status, a.priority, a.created_at, a.updated_at, - i.title AS initiative_title + a.status, a.priority, a.due_at, a.created_at, a.updated_at, + i.title AS initiative_title, + (a.due_at IS NOT NULL AND a.due_at < NOW()) AS is_overdue FROM actions a JOIN action_assignments aa ON aa.action_id = a.id AND aa.tenant_id = a.tenant_id @@ -39,17 +61,13 @@ def _list_assigned_actions( WHERE a.tenant_id = %s AND aa.actor_id = %s AND a.status = ANY(%s) - ORDER BY - CASE a.priority - WHEN 'high' THEN 0 - WHEN 'normal' THEN 1 - WHEN 'low' THEN 2 - END, - a.updated_at DESC + {order_clause} """, (tenant_id, actor_id, statuses), ) actions = [_serialize_row(dict(row)) for row in cur.fetchall()] + for action in actions: + action["is_overdue"] = bool(action.pop("is_overdue", False)) finally: conn.close() return _attach_assignments(actions, tenant_id=tenant_id) @@ -83,3 +101,19 @@ def get_all_my_open_actions(ctx: TenantContext) -> list[dict[str, Any]]: actor_id=actor_id, statuses=list(OPEN_ACTION_STATUSES), ) + + +def get_my_today_actions(ctx: TenantContext, *, limit: int = 15) -> list[dict[str, Any]]: + """Maßnahmen für heute — überfällig zuerst, dann blockiert/review, dann Priorität.""" + if limit < 1: + limit = 1 + if limit > 50: + limit = 50 + actor_id = _require_actor(ctx) + actions = _list_assigned_actions( + tenant_id=ctx.tenant_id, + actor_id=actor_id, + statuses=["open", "ready", "in_progress", "blocked", "review_required"], + order_today=True, + ) + return actions[:limit] diff --git a/backend/data_layer/attention.py b/backend/data_layer/attention.py index 2b043f5..11f9b3e 100644 --- a/backend/data_layer/attention.py +++ b/backend/data_layer/attention.py @@ -567,3 +567,140 @@ def get_next_action_candidates( return candidates[:limit] finally: conn.close() + + +def get_next_action_candidates_for_initiative( + ctx: TenantContext, *, initiative_id: str, limit: int = 5 +) -> list[dict[str, Any]]: + """NextActionCandidates für ein Vorhaben — tenant-scoped.""" + if limit < 1: + limit = 1 + if limit > 20: + limit = 20 + + conn = get_connection() + candidates: list[dict[str, Any]] = [] + try: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute( + """ + SELECT + 'resolve_blocker' AS kind, + b.title AS title, + 'Blocker klären oder Status aktualisieren' AS summary, + b.initiative_id, + b.action_id, + NULL::uuid AS backlog_item_id, + 'blocker_open' AS reason_code, + 'Blocker bearbeiten' AS recommended_action + FROM blockers b + WHERE b.tenant_id = %s AND b.initiative_id = %s + AND b.status IN ('open', 'in_progress') + ORDER BY b.updated_at DESC + LIMIT %s + """, + (ctx.tenant_id, initiative_id, limit), + ) + for row in cur.fetchall(): + item = dict(row) + item["initiative_id"] = str(item["initiative_id"]) + if item.get("action_id"): + item["action_id"] = str(item["action_id"]) + candidates.append(item) + + remaining = limit - len(candidates) + if remaining > 0: + cur.execute( + """ + SELECT + 'assign_action' AS kind, + a.title AS title, + 'Actor zuweisen' AS summary, + a.initiative_id, + a.id AS action_id, + NULL::uuid AS backlog_item_id, + 'action_unassigned' AS reason_code, + 'Maßnahme zuweisen' AS recommended_action + FROM actions a + WHERE a.tenant_id = %s AND a.initiative_id = %s + AND a.status IN ('open', 'ready', 'in_progress') + AND NOT EXISTS ( + SELECT 1 FROM action_assignments aa + WHERE aa.action_id = a.id AND aa.tenant_id = a.tenant_id + ) + ORDER BY a.updated_at DESC + LIMIT %s + """, + (ctx.tenant_id, initiative_id, remaining), + ) + for row in cur.fetchall(): + item = dict(row) + item["initiative_id"] = str(item["initiative_id"]) + item["action_id"] = str(item["action_id"]) + candidates.append(item) + + remaining = limit - len(candidates) + if remaining > 0: + cur.execute( + """ + SELECT + 'convert_backlog' AS kind, + bi.title AS title, + 'Freigegebenes Backlog-Item in Maßnahme umwandeln' AS summary, + bi.initiative_id, + NULL::uuid AS action_id, + bi.id AS backlog_item_id, + 'backlog_accepted_not_converted' AS reason_code, + 'In Maßnahme umwandeln' AS recommended_action + FROM backlog_items bi + WHERE bi.tenant_id = %s AND bi.initiative_id = %s + AND bi.status = 'accepted' + AND bi.converted_action_id IS NULL + ORDER BY bi.updated_at DESC + LIMIT %s + """, + (ctx.tenant_id, initiative_id, remaining), + ) + for row in cur.fetchall(): + item = dict(row) + item["initiative_id"] = str(item["initiative_id"]) + item["backlog_item_id"] = str(item["backlog_item_id"]) + candidates.append(item) + + remaining = limit - len(candidates) + if remaining > 0: + cur.execute( + """ + SELECT 1 FROM actions a + WHERE a.initiative_id = %s AND a.tenant_id = %s + AND a.status IN ('open', 'ready', 'in_progress', 'blocked', 'review_required') + LIMIT 1 + """, + (initiative_id, ctx.tenant_id), + ) + if not cur.fetchone(): + cur.execute( + """ + SELECT title FROM initiatives + WHERE id = %s AND tenant_id = %s + """, + (initiative_id, ctx.tenant_id), + ) + init_row = cur.fetchone() + if init_row: + candidates.append( + { + "kind": "create_action", + "title": init_row["title"], + "summary": "Nächste Maßnahme für Vorhaben anlegen", + "initiative_id": initiative_id, + "action_id": None, + "backlog_item_id": None, + "reason_code": "initiative_no_open_action", + "recommended_action": "Maßnahme anlegen", + } + ) + + return candidates[:limit] + finally: + conn.close() diff --git a/backend/data_layer/initiative_snapshot.py b/backend/data_layer/initiative_snapshot.py index 692cc68..5075539 100644 --- a/backend/data_layer/initiative_snapshot.py +++ b/backend/data_layer/initiative_snapshot.py @@ -10,6 +10,7 @@ from typing import Any, Literal, Optional from psycopg2.extras import RealDictCursor from db import get_connection +from data_layer.attention import get_next_action_candidates_for_initiative from services.initiatives import get_initiative from tenant_context import TenantContext @@ -296,12 +297,27 @@ def get_initiative_steering_snapshot( planned_reviews_due=planned_reviews_due, ) + upcoming_milestones = sorted( + [ + m + for m in milestones + if m["status"] in ("planned", "active", "at_risk") + ], + key=lambda m: (m["target_date"] is None, m["target_date"] or ""), + )[:5] + + next_actions = get_next_action_candidates_for_initiative( + ctx, initiative_id=initiative_id, limit=5 + ) + return { "initiative_id": initiative_id, "initiative_title": initiative["title"], "initiative_status": initiative["status"], "operating_phase": phase, "phase_signals": phase_signals, + "upcoming_milestones": upcoming_milestones, + "next_actions": next_actions, "counts": { "actions_open": open_actions, "actions_blocked": blocked_actions, diff --git a/backend/routers/workspace.py b/backend/routers/workspace.py index 6ab1a24..7e7c990 100644 --- a/backend/routers/workspace.py +++ b/backend/routers/workspace.py @@ -49,6 +49,17 @@ def workspace_blocked_actions( raise HTTPException(status_code=400, detail=str(exc)) from exc +@router.get("/actions/today") +def workspace_today_actions( + limit: Optional[int] = Query(default=15, ge=1, le=50), + ctx: TenantContext = Depends(require_capability("kairo.workspace.read")), +): + try: + return dl_actions.get_my_today_actions(_require_actor_ctx(ctx), limit=limit or 15) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @router.get("/initiatives/active") def workspace_active_initiatives( limit: Optional[int] = Query(default=None, ge=1, le=100), diff --git a/backend/tests/test_ap10b_steering_ui.py b/backend/tests/test_ap10b_steering_ui.py new file mode 100644 index 0000000..97e775f --- /dev/null +++ b/backend/tests/test_ap10b_steering_ui.py @@ -0,0 +1,73 @@ +"""AP0.10b — Steering UI Minimum tests.""" + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone + +from tests.factories import provision_user_in_tenant +from tests.test_initiatives_actions import ( + _auth, + _create_action, + _create_initiative, + _login, +) + + +def test_workspace_next_actions(client): + user = provision_user_in_tenant(tenant_role="member") + token = _login(client, user) + initiative_id = _create_initiative(client, token).json()["id"] + client.post( + f"/api/initiatives/{initiative_id}/blockers", + json={"title": "Blocker für NextAction"}, + headers=_auth(token), + ) + + res = client.get("/api/workspace/next-actions?limit=5", headers=_auth(token)) + assert res.status_code == 200 + items = res.json() + assert len(items) >= 1 + assert items[0]["recommended_action"] + assert items[0]["initiative_id"] + + +def test_workspace_today_actions(client): + user = provision_user_in_tenant(tenant_role="member") + token = _login(client, user) + initiative_id = _create_initiative(client, token).json()["id"] + action = _create_action( + client, token, initiative_id, assigned_actor_ids=[user["actor_id"]] + ).json() + past = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat() + client.patch( + f"/api/actions/{action['id']}", + json={"due_at": past}, + headers=_auth(token), + ) + + res = client.get("/api/workspace/actions/today", headers=_auth(token)) + assert res.status_code == 200 + items = res.json() + assert len(items) >= 1 + assert items[0]["is_overdue"] is True + + +def test_steering_snapshot_v2_fields(client): + user = provision_user_in_tenant(tenant_role="member") + token = _login(client, user) + initiative_id = _create_initiative(client, token).json()["id"] + client.post( + f"/api/initiatives/{initiative_id}/milestones", + json={"title": "M1", "status": "planned", "target_date": "2026-12-31"}, + headers=_auth(token), + ) + + snap = client.get( + f"/api/initiatives/{initiative_id}/steering-snapshot", + headers=_auth(token), + ) + assert snap.status_code == 200 + data = snap.json() + assert "upcoming_milestones" in data + assert "next_actions" in data + assert len(data["upcoming_milestones"]) >= 1 diff --git a/backend/version.py b/backend/version.py index 8fdd795..6e031ce 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,3 +1,3 @@ -APP_VERSION = "0.10.0-ap0.10" +APP_VERSION = "0.10.1-ap0.10b" DB_SCHEMA_VERSION = "008" APP_NAME = "jinkendo-kairo" diff --git a/docs/product/Kairo_MVP_Usability_Recovery_Plan_v0.1.md b/docs/product/Kairo_MVP_Usability_Recovery_Plan_v0.1.md new file mode 100644 index 0000000..70d627f --- /dev/null +++ b/docs/product/Kairo_MVP_Usability_Recovery_Plan_v0.1.md @@ -0,0 +1,189 @@ +# Kairo — MVP Usability Recovery Plan + +**Status:** Planungsdokument (Product Direction) +**Stand:** 2026-07-05 +**Auslöser:** AP0.8–0.10 liefern Entitäten, aber **keinen täglich nutzbaren Steuerungswert** — schlechter als To-do-/PM-Tools für den Alltag. + +--- + +## 1. Diagnose + +| Was Nutzer erwarten | Was Kairo heute liefert | +|---------------------|-------------------------| +| „Was mache ich jetzt?“ | Parallele Listen, keine Priorität in der UI | +| „Wo steht das Vorhaben?“ | Heuristik-Badge ohne Lifecycle-Modell | +| „Was kommt als Nächstes?“ | NextAction-API existiert, **kein Widget** | +| „Was blockiert?“ | Blocker-Liste, kaum mit Maßnahme verbunden | +| Meilensteine / Fristen | CRUD-Sektion, kein Horizont | +| Weniger als Todoist/Linear | **Ja** — mehr Klicks, weniger Antworten | + +Das Product Reset (`Kairo_Product_Definition_and_MVP_Reset_v0.1.md` §4) warnt genau davor: +*Initiative → Action allein ist weniger als To-do-Listen und verfehlt den Program-Director-Anspruch.* + +**Technisch:** Foundation + OM-Inventar ✓ +**Produkt:** Steuerungsantwort auf die Leitfrage ✗ + +--- + +## 2. MVP-Nutzbarkeits-Bar (Definition of Done) + +Kairo ist **minimal täglich nutzbar**, wenn ein Nutzer ohne Erklärung: + +1. **In ≤30 Sekunden** seine nächste sinnvolle Maßnahme findet (Workspace). +2. **Pro Vorhaben** sieht: Phase (grober Zustand), nächster Meilenstein, größtes Hindernis, empfohlene Aktion. +3. **Backlog vs. Maßnahme** unterscheiden und Backlog in Maßnahme überführen kann. +4. **Blocker** erkennt, die echte Arbeit stoppen (nicht nur als Liste). +5. Das **ohne** Prompt/KI/MCP/Method Designer schafft. + +Das ist **nicht** Feature-Parität mit Todoist — sondern **Steuerungsmehrwert** gegenüber reinen Listen: + +> Nicht nur *was* existiert, sondern *was jetzt wirkt*. + +--- + +## 3. Strategische Korrektur der Roadmap + +Die bisherige Reihenfolge: + +```text +AP0.8 Entitäten → AP0.9 Entitäten → AP0.10 Validation → Sprint 1 Steering Core +``` + +**Problem:** Validation auf CRUD-Listen liefert nur „fehlt alles“ — ohne vorherige **Steering Surface**. + +**Korrigierte Reihenfolge:** + +```text +AP0.10b Steering UI Minimum ← zuerst (Nutzen sichtbar) +AP0.10c Initiative Flow Minimum +AP0.10d MVP Validation (5 Vorhaben) +AP1.0 Steering Core Foundation (nur mit Validation-Ergebnis) +AP1.1 Method Registry minimal +``` + +**Regel bis AP1.0:** Keine neuen OM-Tabellen. Nur verbinden, anzeigen, orchestrieren, validieren. + +--- + +## 4. Empfohlene Pakete + +### AP0.10b — Steering UI Minimum (Priorität 1) + +**Ziel:** Leitfrage im Workspace beantworten. + +| # | Lieferung | Aufwand | Nutzen | +|---|-----------|---------|--------| +| 1 | **NextActionWidget** — `/api/workspace/next-actions`, Top 5, Link zu Vorhaben/Maßnahme | S | ★★★ | +| 2 | **Workspace „Heute“** — meine offenen Maßnahmen sortiert: überfällig → blockiert → high priority | S | ★★★ | +| 3 | **Steuerungszustand erweitern** — nächster Meilenstein (`target_date`), Top-Next-Action *für dieses Vorhaben*, Klartext statt `work_in_progress` | M | ★★★ | +| 4 | Attention-Widget: Kind + empfohlene Aktion lesbar | S | ★★ | + +**Nicht-Scope:** Redesign, neue Entitäten, KI-Ranking. + +--- + +### AP0.10c — Initiative Flow Minimum (Priorität 2) + +**Ziel:** Vorhaben-Detail beantwortet „Wo stehe ich?“ — nicht „Hier sind 8 Listen“. + +| # | Lieferung | Aufwand | Nutzen | +|---|-----------|---------|--------| +| 1 | **Maßnahmen-zentrierte Ansicht** — Blocker/Evidence/Reviews unter der Maßnahme, nicht nur global | M | ★★★ | +| 2 | **Sektionen einklappen** — Backlog/Evidence/Decision/Recurring unter „Erweitert“ | S | ★★ | +| 3 | **Meilenstein-Horizont** — „Als Nächstes: …“ (planned/active, sortiert nach `target_date`) | S | ★★ | +| 4 | **Flows sichtbar machen** — Blocker→blockiert, Review→review_required→done (Hinweis in UI) | S | ★★ | +| 5 | Snapshot-API in UI: `next_actions[]`, `upcoming_milestones[]` Felder | M | ★★★ | + +--- + +### AP0.10d — MVP Validation (Priorität 3) + +**Ziel:** Mit echten Vorhaben prüfen — dokumentiert, nicht Bauchgefühl. + +Testvorhaben (Roadmap): Kairo, Gewaltschutzkurs, Karate, Familienorganisation, Server/Mindnet. + +**Checkliste pro Vorhaben (15 Min):** + +- [ ] Nächste Aktion in ≤30s gefunden? +- [ ] Blocker früh sichtbar? +- [ ] Meilenstein-Horizont hilfreich? +- [ ] Backlog→Maßnahme sinnvoll? +- [ ] Besser als Todo-Tool für *dieses* Vorhaben? Wenn nein: was fehlt? + +**Output:** `Sprint0_AP0_10_Validation_Report_v0.1.md` + Go/No-Go für AP1.0. + +--- + +### AP1.0 — Steering Core Foundation (Priorität 4, nach Validation) + +**Ziel:** Heuristik durch echte Steuerungsstruktur ersetzen — schmal starten. + +| Baustein | Minimal-Inhalt | +|----------|----------------| +| `SteeringContext` | `initiative_id`, `method_key` (default `generic_program`), `lifecycle_step` | +| Lifecycle | 5 Schritte: `intake`, `planning`, `execution`, `review`, `closure` | +| `operating_phase` | Liest aus `lifecycle_step`, nicht nur Zähler | +| Hooks (stub) | `on_blocker_open`, `on_next_action_requested` — Logging only | +| Migration | `009_steering_context.sql` | + +**Nicht in AP1.0:** Method Designer, Workflow Runtime, Dependencies, Sprints. + +--- + +### AP1.1 — Method Registry minimal (Priorität 5) + +- Eine eingebaute Methode: **`generic_program`** +- Signal Rules + NextAction Strategy für diese Methode +- Später: `software_delivery`, `content_development`, … + +--- + +## 5. Was bewusst warten muss + +| Thema | Warum später | +|-------|--------------| +| Project-Entität | Erst wenn Initiative-Steuerung trägt | +| Abhängigkeiten / Gantt | Steering Core + Validation | +| Sprints / Work Cycles | Method Registry | +| KI-Priorisierung | Regelbasiert muss zuerst gut genug sein | +| UI-Redesign | Erst wenn Informationsarchitektur steht | +| Weitere OM-Tabellen | Stop — Inventar reicht | + +--- + +## 6. Erfolgskriterien (Go für Sprint 1) + +Sprint 1 (Steering Core) startet nur wenn: + +1. **≥3 von 5** Testvorhaben: „Hilft bei täglicher Steuerung“ = ja (schwach ja reicht nicht). +2. **Leitfrage** auf Workspace in User-Test beantwortbar ohne Schulung. +3. **NextActionWidget** und erweiterter Steuerungszustand deployed und genutzt. +4. Validation-Report dokumentiert verbleibende Lücken — keine stillen Annahmen. + +--- + +## 7. Empfohlene unmittelbare Reihenfolge (nächste 2–3 Umsetzungssprints) + +```text +Sprint A (AP0.10b) NextActionWidget + Workspace Heute + Steuerungszustand v2 +Sprint B (AP0.10c) Maßnahmen-Hub + Meilenstein-Horizont + UI entrauschen +Sprint C (AP0.10d) Validation + Report + AP1.0 Auftrag schärfen +Sprint D (AP1.0) SteeringContext + Lifecycle stub +``` + +**Sofort starten mit Sprint A** — größter Hebel, kleinster Scope, API existiert bereits. + +--- + +## 8. Einordnung: Kairo vs. To-do-Tool + +Kairo muss **nicht** mehr Features als Todoist haben. +Es muss **eine Frage besser beantworten**: + +> *Welcher nächste Schritt bringt dieses Vorhaben jetzt am wirksamsten voran — und warum?* + +Solange diese Frage in der UI nicht zentral beantwortet wird, ist jeder OM-Ausbau wertlos. + +--- + +*v0.1 — Product Direction, ergänzt Corrected MVP Roadmap AP0.10* diff --git a/docs/sprints/Sprint0_AP0_10b_Assignment_v0.1.md b/docs/sprints/Sprint0_AP0_10b_Assignment_v0.1.md new file mode 100644 index 0000000..2304d1b --- /dev/null +++ b/docs/sprints/Sprint0_AP0_10b_Assignment_v0.1.md @@ -0,0 +1,31 @@ +# AP0.10b — Steering UI Minimum +## Implementierungsauftrag v0.1 + +**Status:** umgesetzt +**Stand:** 2026-07-05 +**Version:** `0.10.1-ap0.10b` · Schema `008` +**Basis:** `Kairo_MVP_Usability_Recovery_Plan_v0.1.md` + +--- + +## Ziel + +Leitfrage im Workspace beantworten — **ohne** neue Tabellen, **ohne** Steering Core. + +## Geliefert + +| # | Lieferung | +|---|-----------| +| 1 | **NextActionWidget** — Top 5, Link zum Vorhaben | +| 2 | **WorkspaceTodayWidget** — überfällig → blockiert → Priorität | +| 3 | **Steuerungszustand v2** — Phase-Klartext, Meilenstein-Horizont, Next Actions pro Vorhaben | +| 4 | **Attention** — Kind-Labels lesbar | +| 5 | API `/api/workspace/actions/today`, Snapshot-Felder `upcoming_milestones`, `next_actions` | + +## Abnahme + +Workspace zeigt oben Next Actions + Heute; Vorhaben-Detail zeigt erweiterten Steuerungszustand; pytest `test_ap10b_steering_ui.py`. + +--- + +*Nächster Schritt: AP0.10c Initiative Flow Minimum* diff --git a/frontend/package.json b/frontend/package.json index c47ccd0..2365355 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "kairo-jinkendo-frontend", - "version": "0.10.0-ap0.10", + "version": "0.10.1-ap0.10b", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/api/workspace.js b/frontend/src/api/workspace.js index b699e53..88b3efd 100644 --- a/frontend/src/api/workspace.js +++ b/frontend/src/api/workspace.js @@ -12,6 +12,10 @@ export function listWorkspaceBlockedActions() { return apiFetch('/api/workspace/actions/blocked') } +export function listWorkspaceTodayActions(limit = 15) { + return apiFetch(`/api/workspace/actions/today?limit=${limit}`) +} + export function listWorkspaceActiveInitiatives(limit = 5) { return apiFetch(`/api/workspace/initiatives/active?limit=${limit}`) } diff --git a/frontend/src/components/SteeringSnapshotPanel.jsx b/frontend/src/components/SteeringSnapshotPanel.jsx index 7bf3ab3..609ff0d 100644 --- a/frontend/src/components/SteeringSnapshotPanel.jsx +++ b/frontend/src/components/SteeringSnapshotPanel.jsx @@ -1,4 +1,20 @@ -import { PHASE_LABELS } from '../constants/operating.js' +import { Link } from 'react-router-dom' +import { + PHASE_LABELS, + PHASE_DESCRIPTIONS, + SIGNAL_LABELS, + NEXT_ACTION_KIND_LABELS, +} from '../constants/operating.js' +import { MILESTONE_STATUS_LABELS } from '../constants/status.js' + +function formatDate(iso) { + if (!iso) return '—' + try { + return new Date(iso).toLocaleDateString('de-DE') + } catch { + return iso + } +} export function SteeringSnapshotPanel({ snapshot, loading, error }) { if (loading) { @@ -22,8 +38,15 @@ export function SteeringSnapshotPanel({ snapshot, loading, error }) { ) } - const { operating_phase, phase_signals, counts } = snapshot + const { + operating_phase, + phase_signals, + counts, + upcoming_milestones = [], + next_actions = [], + } = snapshot const phaseLabel = PHASE_LABELS[operating_phase] || operating_phase + const phaseDesc = PHASE_DESCRIPTIONS[operating_phase] || '' return (
@@ -31,11 +54,54 @@ export function SteeringSnapshotPanel({ snapshot, loading, error }) {

Steuerungszustand

{phaseLabel} + + {phaseDesc &&

{phaseDesc}

} + {phase_signals?.length > 0 && (

- Signale: {phase_signals.join(' · ')} + {phase_signals.map((s) => SIGNAL_LABELS[s] || s).join(' · ')}

)} + + {next_actions.length > 0 && ( +
+

Nächste Schritte für dieses Vorhaben

+
    + {next_actions.map((item, i) => ( +
  1. +
    + {i + 1} + {item.title} + + {NEXT_ACTION_KIND_LABELS[item.kind] || item.kind} + {item.recommended_action ? ` — ${item.recommended_action}` : ''} + +
    +
  2. + ))} +
+
+ )} + + {upcoming_milestones.length > 0 && ( +
+

Meilenstein-Horizont

+ +
+ )} +
Offene Maßnahmen
@@ -62,8 +128,9 @@ export function SteeringSnapshotPanel({ snapshot, loading, error }) {
)}
+

- Kanonischer Operating Cycle — noch heuristisch, später methodengeführt über Steering Core. + Heuristischer Steuerungszustand — später methodengeführt über Steering Core.

) diff --git a/frontend/src/constants/operating.js b/frontend/src/constants/operating.js index cb6284d..07ca56c 100644 --- a/frontend/src/constants/operating.js +++ b/frontend/src/constants/operating.js @@ -21,3 +21,48 @@ export const PHASE_LABELS = { adapt: 'Anpassung', closure: 'Abschluss', } + +export const PHASE_DESCRIPTIONS = { + capture: 'Ideen und Eingänge erfassen — noch wenig strukturierte Arbeit.', + triage: 'Backlog wartet auf Commit — noch keine offene nächste Maßnahme.', + structure: 'Vorhaben braucht Struktur — keine offene Maßnahme, Backlog leer.', + commit: 'Maßnahmen committen und zuweisen.', + execute: 'Aktive Umsetzung — offene Maßnahmen oder Blocker bearbeiten.', + verify: 'Nachweise und Ergebnisse belegen.', + review: 'Reviews oder Maßnahmen mit Review-Status klären.', + adapt: 'Anpassen — Backlog, Meilensteine oder Prioritäten prüfen.', + closure: 'Vorhaben abgeschlossen oder archiviert.', +} + +export const SIGNAL_LABELS = { + initiative_terminal: 'Vorhaben abgeschlossen', + backlog_awaiting_commit: 'Backlog wartet auf Maßnahme', + no_open_work: 'Keine offene Arbeit', + actions_blocked: 'Maßnahmen blockiert', + open_blockers: 'Offene Blocker', + actions_need_review: 'Maßnahmen brauchen Review', + reviews_due: 'Reviews fällig', + work_in_progress: 'Arbeit in Umsetzung', +} + +export const NEXT_ACTION_KIND_LABELS = { + resolve_blocker: 'Blocker klären', + assign_action: 'Maßnahme zuweisen', + convert_backlog: 'Backlog umwandeln', + create_action: 'Maßnahme anlegen', + review_milestone: 'Meilenstein prüfen', +} + +export const ATTENTION_KIND_LABELS = { + blocked_action: 'Blockierte Maßnahme', + open_blocker: 'Offener Blocker', + high_priority_action: 'High Priority', + unassigned_action: 'Ohne Zuweisung', + initiative_without_next_action: 'Keine nächste Maßnahme', + stale_initiative: 'Inaktives Vorhaben', + milestone_at_risk: 'Meilenstein gefährdet', + overdue_action: 'Überfällig', + review_due: 'Review fällig', + recurring_due: 'Wiederkehrend fällig', + action_review_required: 'Review ausstehend', +} diff --git a/frontend/src/pages/WorkspacePage.jsx b/frontend/src/pages/WorkspacePage.jsx index ed52d02..a6b307e 100644 --- a/frontend/src/pages/WorkspacePage.jsx +++ b/frontend/src/pages/WorkspacePage.jsx @@ -38,7 +38,7 @@ export function WorkspacePage() {

Workspace

-

Dein Überblick über Vorhaben und Maßnahmen.

+

Was ist dein nächster sinnvoller Schritt?

{hasCapability('kairo.initiative.manage') && ( + } + > + {loading && } + {!loading && error && } + {!loading && !error && items.length === 0 && ( + + )} + {!loading && !error && items.length > 0 && ( +
    + {items.map((item, index) => ( +
  1. +
    + {index + 1} + + {NEXT_ACTION_KIND_LABELS[item.kind] || item.kind} + + {item.title} + {item.summary &&

    {item.summary}

    } + {item.recommended_action && ( +

    + → {item.recommended_action} +

    + )} +
    +
    + {item.initiative_id && ( + + Zum Vorhaben + + )} +
    +
  2. + ))} +
+ )} + + ) +} diff --git a/frontend/src/widgets/WorkspaceTodayWidget.jsx b/frontend/src/widgets/WorkspaceTodayWidget.jsx new file mode 100644 index 0000000..4fca643 --- /dev/null +++ b/frontend/src/widgets/WorkspaceTodayWidget.jsx @@ -0,0 +1,98 @@ +import { useCallback, useEffect, useState } from 'react' +import { Link } from 'react-router-dom' +import { listWorkspaceTodayActions } from '../api/workspace.js' +import { StatusBadge } from '../components/StatusBadge.jsx' +import { PriorityBadge } from '../components/PriorityBadge.jsx' +import { EmptyState } from '../components/EmptyState.jsx' +import { ErrorState } from '../components/ErrorState.jsx' +import { LoadingState } from '../components/LoadingState.jsx' +import { WidgetCard } from '../components/WidgetCard.jsx' + +function formatDue(iso) { + if (!iso) return null + try { + return new Date(iso).toLocaleString('de-DE', { + day: '2-digit', + month: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) + } catch { + return iso + } +} + +export function WorkspaceTodayWidget() { + const [actions, setActions] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const load = useCallback(async () => { + setLoading(true) + setError(null) + try { + const data = await listWorkspaceTodayActions(10) + setActions(Array.isArray(data) ? data : []) + } catch (err) { + setError(err.message) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + load() + }, [load]) + + return ( + + Aktualisieren + + } + footer={ + actions.length > 0 ? ( + + Alle meine Maßnahmen → + + ) : null + } + > + {loading && } + {!loading && error && } + {!loading && !error && actions.length === 0 && ( + + )} + {!loading && !error && actions.length > 0 && ( +
    + {actions.map((action) => ( +
  • +
    + {action.is_overdue && ( + Überfällig + )} + {action.title} + {action.initiative_title && ( + {action.initiative_title} + )} + {action.due_at && ( + Fällig: {formatDue(action.due_at)} + )} +
    +
    + + + + Öffnen + +
    +
  • + ))} +
+ )} +
+ ) +}