Kairo-Jinkendo/backend/services/maturity_practice.py
Lars 3a0ef3a734
Some checks failed
Deploy Development / deploy (push) Successful in 52s
Test Suite / pytest-backend (push) Failing after 5m3s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 3s
Test Suite / compose-smoke (push) Has been skipped
feat(A1): Progression Join-Graph und Multi-Strang-Horizon
join_gate, strand_project_id, Join-Auto-Schalten, multi-active Gates, Progression-Demo und PO-Lock-Spec. FE: Today strang-gruppiert, Join-Warteliste in Kontrolle.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 16:28:08 +02:00

229 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""A1 — Stage-bound practice exercises (Activity Set)."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any, Optional
from services.recurring import (
create_recurring_element,
list_recurring_for_initiative,
update_recurring_element,
)
# Starter exercise sets per stage title (Spagat reference path).
STAGE_PRACTICE_SETS: dict[str, list[tuple[str, str]]] = {
"Stufe 1 — Basis": [
("Vorbeuge-Dehnung", "Täglich 3×30 Sek. — Hüfte und Beinrückseite"),
("Hüftöffner (Schmetterling)", "Täglich sitzend, leichter Druck"),
],
"Stufe 2 — Aufbau": [
("Seitlicher Spagat — links", "Täglich halten, optional Messwert (Grad)"),
("Seitlicher Spagat — rechts", "Täglich halten, optional Messwert (Grad)"),
("Aktive Vorbeuge", "Täglich 3× kurz halten"),
],
"Stufe 3 — Ziel": [
("Voller Spagat", "Täglich Zielposition — Messwert dokumentieren"),
],
"Dehnung — Einstieg": [
("Vorbeuge halten", "Täglich 3×30 Sek."),
],
"Hüftrotation — Einstieg": [
("Hüftkreisen im Stand", "Täglich 10 Wiederholungen je Richtung"),
],
"Dehnung — nach Join": [
("Enddehnung Mawashi-Vorbereitung", "Täglich 2×45 Sek."),
],
"Hüftrotation — nach Join": [
("Explosive Hüftrotation", "Täglich 3×8 Wiederholungen"),
],
}
def get_active_maturity_stages(
*, tenant_id: str, initiative_id: str
) -> list[dict[str, Any]]:
from services.roadmap import list_roadmap_items_for_initiative
stages = [
item
for item in list_roadmap_items_for_initiative(
tenant_id=tenant_id, initiative_id=initiative_id
)
if item.get("item_type") == "maturity_stage" and item.get("status") == "active"
]
stages.sort(key=lambda s: (s.get("sort_order", 0), s.get("title", "")))
return stages
def get_active_maturity_stage(
*, tenant_id: str, initiative_id: str
) -> Optional[dict[str, Any]]:
stages = get_active_maturity_stages(
tenant_id=tenant_id, initiative_id=initiative_id
)
return stages[0] if stages else None
def list_practices_for_stage(
*,
tenant_id: str,
initiative_id: str,
stage_id: str,
) -> list[dict[str, Any]]:
items = list_recurring_for_initiative(
tenant_id=tenant_id, initiative_id=initiative_id
)
return [
item
for item in items
if item.get("roadmap_item_id") == stage_id
]
def pause_all_practices(
*,
tenant_id: str,
initiative_id: str,
user_id: Optional[str] = None,
) -> list[str]:
paused: list[str] = []
for item in list_recurring_for_initiative(
tenant_id=tenant_id, initiative_id=initiative_id
):
if item.get("status") == "active":
update_recurring_element(
tenant_id=tenant_id,
recurring_id=item["id"],
status="paused",
user_id=user_id,
)
paused.append(item["title"])
return paused
def pause_practices_except_stage(
*,
tenant_id: str,
initiative_id: str,
keep_stage_id: str,
strand_project_id: Optional[str] = None,
user_id: Optional[str] = None,
) -> list[str]:
paused: list[str] = []
for item in list_recurring_for_initiative(
tenant_id=tenant_id, initiative_id=initiative_id
):
if item.get("status") != "active":
continue
if item.get("roadmap_item_id") == keep_stage_id:
continue
if strand_project_id is not None and item.get("project_id") != strand_project_id:
continue
update_recurring_element(
tenant_id=tenant_id,
recurring_id=item["id"],
status="paused",
user_id=user_id,
)
paused.append(item["title"])
return paused
def seed_practices_for_stage(
*,
tenant_id: str,
initiative_id: str,
stage: dict[str, Any],
user_id: Optional[str] = None,
) -> list[dict[str, Any]]:
"""Idempotent: create missing exercises for a stage; activate existing."""
stage_id = stage["id"]
stage_title = stage.get("title") or ""
strand_project_id = stage.get("strand_project_id")
defs = STAGE_PRACTICE_SETS.get(stage_title)
if not defs:
defs = [
(
f"Übung — {stage_title}",
f"Tägliche Routine für {stage_title}",
)
]
existing = list_practices_for_stage(
tenant_id=tenant_id,
initiative_id=initiative_id,
stage_id=stage_id,
)
existing_by_title = {item["title"]: item for item in existing}
created: list[dict[str, Any]] = []
now = datetime.now(timezone.utc)
for title, description in defs:
if title in existing_by_title:
item = existing_by_title[title]
if item.get("status") != "active":
updated = update_recurring_element(
tenant_id=tenant_id,
recurring_id=item["id"],
status="active",
next_due_at=now,
user_id=user_id,
)
if updated:
created.append(updated)
else:
created.append(item)
continue
row = create_recurring_element(
tenant_id=tenant_id,
initiative_id=initiative_id,
title=title,
description=description,
status="active",
interval_days=1,
next_due_at=now,
roadmap_item_id=stage_id,
project_id=strand_project_id,
user_id=user_id,
)
created.append(row)
return created
def activate_stage_practice_set(
*,
tenant_id: str,
initiative_id: str,
stage: dict[str, Any],
user_id: Optional[str] = None,
) -> dict[str, Any]:
strand_project_id = stage.get("strand_project_id")
if strand_project_id:
paused = pause_practices_except_stage(
tenant_id=tenant_id,
initiative_id=initiative_id,
keep_stage_id=stage["id"],
strand_project_id=strand_project_id,
user_id=user_id,
)
else:
paused = pause_all_practices(
tenant_id=tenant_id, initiative_id=initiative_id, user_id=user_id
)
exercises = seed_practices_for_stage(
tenant_id=tenant_id,
initiative_id=initiative_id,
stage=stage,
user_id=user_id,
)
return {
"stage_id": stage["id"],
"stage_title": stage.get("title"),
"paused_practices": paused,
"active_practices": [e["title"] for e in exercises],
"practice_ids": [e["id"] for e in exercises],
}