Kairo-Jinkendo/backend/services/maturity_practice.py
Lars a05bd34af3
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 5m3s
Test Suite / lint-backend (push) Successful in 3s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 19s
Test Suite / playwright-smoke (push) Successful in 13s
feat(A1): Übungen als AP mit Wochentagsplan
Gate- und Kit-Übungen legen ein Arbeitspaket plus Schedule an; Today zeigt nur fällige Wochentage, nicht die ganze Gate-Liste.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 18:06:41 +02:00

249 lines
7.1 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 (
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", "3×30 Sek. — Hüfte und Beinrückseite"),
("Hüftöffner (Schmetterling)", "Sitzend, leichter Druck"),
],
"Stufe 2 — Aufbau": [
("Seitlicher Spagat — links", "Halten, optional Messwert (Grad)"),
("Seitlicher Spagat — rechts", "Halten, optional Messwert (Grad)"),
("Aktive Vorbeuge", "3× kurz halten"),
],
"Stufe 3 — Ziel": [
("Voller Spagat", "Zielposition — Messwert dokumentieren"),
],
"Dehnung — Einstieg": [
("Vorbeuge halten", "3×30 Sek."),
],
"Hüftrotation — Einstieg": [
("Hüftkreisen im Stand", "10 Wiederholungen je Richtung"),
],
"Dehnung — nach Join": [
("Enddehnung Mawashi-Vorbereitung", "2×45 Sek."),
],
"Hüftrotation — nach Join": [
("Explosive Hüftrotation", "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_on_same_lane_except_stage(
*,
tenant_id: str,
initiative_id: str,
keep_stage_id: str,
user_id: Optional[str] = None,
) -> list[str]:
from services.progression_graph import progression_lanes_for_initiative
lanes = progression_lanes_for_initiative(
tenant_id=tenant_id, initiative_id=initiative_id
)
keep_lane = lanes.get(str(keep_stage_id))
if not keep_lane:
return pause_all_practices(
tenant_id=tenant_id, initiative_id=initiative_id, user_id=user_id
)
lane_gate_ids = {
gate_id for gate_id, lane in lanes.items() if lane == keep_lane
}
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 str(item.get("roadmap_item_id") or "") not in lane_gate_ids:
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 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]:
del strand_project_id
return pause_practices_on_same_lane_except_stage(
tenant_id=tenant_id,
initiative_id=initiative_id,
keep_stage_id=keep_stage_id,
user_id=user_id,
)
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 ""
defs = STAGE_PRACTICE_SETS.get(stage_title)
if not defs:
defs = [
(
f"Übung — {stage_title}",
f"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)
from services.scheduled_work import create_scheduled_action_at_gate
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_scheduled_action_at_gate(
tenant_id=tenant_id,
initiative_id=initiative_id,
gate_id=stage_id,
title=title,
description=description,
user_id=user_id,
schedule_kind="weekdays",
)
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]:
paused = pause_practices_on_same_lane_except_stage(
tenant_id=tenant_id,
initiative_id=initiative_id,
keep_stage_id=stage["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],
}