feat: flexible rest days system with JSONB config (v9d Phase 2a)
PROBLEM: Simple full_rest/active_recovery model doesn't support
context-specific rest days (e.g., strength rest but cardio allowed).
SOLUTION: JSONB-based flexible rest day configuration.
## Changes:
**Migration 010:**
- Refactor rest_days.type → rest_config JSONB
- Schema: {focus, rest_from[], allows[], intensity_max}
- Validation function with check constraint
- GIN index for performant JSONB queries
**Backend (routers/rest_days.py):**
- CRUD: list, create (upsert by date), get, update, delete
- Stats: count per week, focus distribution
- Validation: check activity conflicts with rest day config
**Frontend (api.js):**
- 7 new methods: listRestDays, createRestDay, updateRestDay,
deleteRestDay, getRestDaysStats, validateActivity
**Integration:**
- Router registered in main.py
- Ready for weekly planning validation rules
## Next Steps:
- Frontend UI (RestDaysPage with Quick/Custom mode)
- Activity conflict warnings
- Dashboard widget
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 16:20:52 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Rest Days Endpoints for Mitai Jinkendo
|
|
|
|
|
|
|
|
|
|
|
|
Context-specific rest days with flexible JSONB configuration.
|
|
|
|
|
|
"""
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from typing import Optional, Literal
|
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, HTTPException, Depends, Header
|
|
|
|
|
|
from pydantic import BaseModel, Field
|
2026-03-22 16:38:39 +01:00
|
|
|
|
from psycopg2.extras import Json
|
2026-03-22 17:36:49 +01:00
|
|
|
|
from psycopg2.errors import UniqueViolation
|
feat: flexible rest days system with JSONB config (v9d Phase 2a)
PROBLEM: Simple full_rest/active_recovery model doesn't support
context-specific rest days (e.g., strength rest but cardio allowed).
SOLUTION: JSONB-based flexible rest day configuration.
## Changes:
**Migration 010:**
- Refactor rest_days.type → rest_config JSONB
- Schema: {focus, rest_from[], allows[], intensity_max}
- Validation function with check constraint
- GIN index for performant JSONB queries
**Backend (routers/rest_days.py):**
- CRUD: list, create (upsert by date), get, update, delete
- Stats: count per week, focus distribution
- Validation: check activity conflicts with rest day config
**Frontend (api.js):**
- 7 new methods: listRestDays, createRestDay, updateRestDay,
deleteRestDay, getRestDaysStats, validateActivity
**Integration:**
- Router registered in main.py
- Ready for weekly planning validation rules
## Next Steps:
- Frontend UI (RestDaysPage with Quick/Custom mode)
- Activity conflict warnings
- Dashboard widget
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 16:20:52 +01:00
|
|
|
|
|
|
|
|
|
|
from db import get_db, get_cursor, r2d
|
|
|
|
|
|
from auth import require_auth
|
|
|
|
|
|
from routers.profiles import get_pid
|
|
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api/rest-days", tags=["rest-days"])
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Models ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
class RestConfig(BaseModel):
|
|
|
|
|
|
focus: Literal['muscle_recovery', 'cardio_recovery', 'mental_rest', 'deload', 'injury']
|
|
|
|
|
|
rest_from: list[str] = Field(default_factory=list, description="Training type IDs to avoid")
|
|
|
|
|
|
allows: list[str] = Field(default_factory=list, description="Allowed activity type IDs")
|
|
|
|
|
|
intensity_max: Optional[int] = Field(None, ge=1, le=100, description="Max HR% for allowed activities")
|
|
|
|
|
|
note: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RestDayCreate(BaseModel):
|
|
|
|
|
|
date: str # YYYY-MM-DD
|
|
|
|
|
|
rest_config: RestConfig
|
|
|
|
|
|
note: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RestDayUpdate(BaseModel):
|
|
|
|
|
|
date: Optional[str] = None
|
|
|
|
|
|
rest_config: Optional[RestConfig] = None
|
|
|
|
|
|
note: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ActivityConflictCheck(BaseModel):
|
|
|
|
|
|
date: str
|
|
|
|
|
|
activity_type: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── CRUD Endpoints ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("")
|
|
|
|
|
|
def list_rest_days(
|
|
|
|
|
|
limit: int = 90,
|
|
|
|
|
|
x_profile_id: Optional[str] = Header(default=None),
|
|
|
|
|
|
session: dict = Depends(require_auth)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""List rest days for current profile (last N days)."""
|
|
|
|
|
|
pid = get_pid(x_profile_id)
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT id, profile_id, date, rest_config, note, created_at
|
|
|
|
|
|
FROM rest_days
|
|
|
|
|
|
WHERE profile_id = %s
|
|
|
|
|
|
ORDER BY date DESC
|
|
|
|
|
|
LIMIT %s
|
|
|
|
|
|
""",
|
|
|
|
|
|
(pid, limit)
|
|
|
|
|
|
)
|
|
|
|
|
|
return [r2d(r) for r in cur.fetchall()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("")
|
|
|
|
|
|
def create_rest_day(
|
|
|
|
|
|
data: RestDayCreate,
|
|
|
|
|
|
x_profile_id: Optional[str] = Header(default=None),
|
|
|
|
|
|
session: dict = Depends(require_auth)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""Create rest day with JSONB config. Upserts by date."""
|
|
|
|
|
|
pid = get_pid(x_profile_id)
|
|
|
|
|
|
|
|
|
|
|
|
# Validate date format
|
|
|
|
|
|
try:
|
|
|
|
|
|
datetime.strptime(data.date, '%Y-%m-%d')
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
raise HTTPException(400, "Invalid date format. Use YYYY-MM-DD")
|
|
|
|
|
|
|
|
|
|
|
|
# Convert RestConfig to dict for JSONB storage
|
|
|
|
|
|
config_dict = data.rest_config.model_dump()
|
2026-03-22 17:36:49 +01:00
|
|
|
|
focus = data.rest_config.focus
|
feat: flexible rest days system with JSONB config (v9d Phase 2a)
PROBLEM: Simple full_rest/active_recovery model doesn't support
context-specific rest days (e.g., strength rest but cardio allowed).
SOLUTION: JSONB-based flexible rest day configuration.
## Changes:
**Migration 010:**
- Refactor rest_days.type → rest_config JSONB
- Schema: {focus, rest_from[], allows[], intensity_max}
- Validation function with check constraint
- GIN index for performant JSONB queries
**Backend (routers/rest_days.py):**
- CRUD: list, create (upsert by date), get, update, delete
- Stats: count per week, focus distribution
- Validation: check activity conflicts with rest day config
**Frontend (api.js):**
- 7 new methods: listRestDays, createRestDay, updateRestDay,
deleteRestDay, getRestDaysStats, validateActivity
**Integration:**
- Router registered in main.py
- Ready for weekly planning validation rules
## Next Steps:
- Frontend UI (RestDaysPage with Quick/Custom mode)
- Activity conflict warnings
- Dashboard widget
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 16:20:52 +01:00
|
|
|
|
|
2026-03-22 17:36:49 +01:00
|
|
|
|
try:
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
# Insert (multiple entries per date allowed, but not same focus)
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO rest_days (profile_id, date, focus, rest_config, note, created_at)
|
|
|
|
|
|
VALUES (%s, %s, %s, %s, %s, CURRENT_TIMESTAMP)
|
|
|
|
|
|
RETURNING id, profile_id, date, focus, rest_config, note, created_at
|
|
|
|
|
|
""",
|
|
|
|
|
|
(pid, data.date, focus, Json(config_dict), data.note)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = cur.fetchone()
|
|
|
|
|
|
return r2d(result)
|
|
|
|
|
|
except UniqueViolation:
|
|
|
|
|
|
# User-friendly error for duplicate focus
|
|
|
|
|
|
focus_labels = {
|
|
|
|
|
|
'muscle_recovery': 'Muskelregeneration',
|
|
|
|
|
|
'cardio_recovery': 'Cardio-Erholung',
|
|
|
|
|
|
'mental_rest': 'Mentale Erholung',
|
|
|
|
|
|
'deload': 'Deload',
|
|
|
|
|
|
'injury': 'Verletzungspause',
|
|
|
|
|
|
}
|
|
|
|
|
|
focus_label = focus_labels.get(focus, focus)
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
400,
|
|
|
|
|
|
f"Du hast bereits einen Ruhetag '{focus_label}' für {data.date}. Bitte wähle einen anderen Typ oder lösche den bestehenden Eintrag."
|
feat: flexible rest days system with JSONB config (v9d Phase 2a)
PROBLEM: Simple full_rest/active_recovery model doesn't support
context-specific rest days (e.g., strength rest but cardio allowed).
SOLUTION: JSONB-based flexible rest day configuration.
## Changes:
**Migration 010:**
- Refactor rest_days.type → rest_config JSONB
- Schema: {focus, rest_from[], allows[], intensity_max}
- Validation function with check constraint
- GIN index for performant JSONB queries
**Backend (routers/rest_days.py):**
- CRUD: list, create (upsert by date), get, update, delete
- Stats: count per week, focus distribution
- Validation: check activity conflicts with rest day config
**Frontend (api.js):**
- 7 new methods: listRestDays, createRestDay, updateRestDay,
deleteRestDay, getRestDaysStats, validateActivity
**Integration:**
- Router registered in main.py
- Ready for weekly planning validation rules
## Next Steps:
- Frontend UI (RestDaysPage with Quick/Custom mode)
- Activity conflict warnings
- Dashboard widget
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 16:20:52 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{rest_day_id}")
|
|
|
|
|
|
def get_rest_day(
|
|
|
|
|
|
rest_day_id: int,
|
|
|
|
|
|
x_profile_id: Optional[str] = Header(default=None),
|
|
|
|
|
|
session: dict = Depends(require_auth)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""Get single rest day by ID."""
|
|
|
|
|
|
pid = get_pid(x_profile_id)
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT id, profile_id, date, rest_config, note, created_at
|
|
|
|
|
|
FROM rest_days
|
|
|
|
|
|
WHERE id = %s AND profile_id = %s
|
|
|
|
|
|
""",
|
|
|
|
|
|
(rest_day_id, pid)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
row = cur.fetchone()
|
|
|
|
|
|
if not row:
|
|
|
|
|
|
raise HTTPException(404, "Rest day not found")
|
|
|
|
|
|
|
|
|
|
|
|
return r2d(row)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/{rest_day_id}")
|
|
|
|
|
|
def update_rest_day(
|
|
|
|
|
|
rest_day_id: int,
|
|
|
|
|
|
data: RestDayUpdate,
|
|
|
|
|
|
x_profile_id: Optional[str] = Header(default=None),
|
|
|
|
|
|
session: dict = Depends(require_auth)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""Update rest day."""
|
|
|
|
|
|
pid = get_pid(x_profile_id)
|
|
|
|
|
|
|
|
|
|
|
|
# Build update fields dynamically
|
|
|
|
|
|
updates = []
|
|
|
|
|
|
values = []
|
|
|
|
|
|
|
|
|
|
|
|
if data.date:
|
|
|
|
|
|
try:
|
|
|
|
|
|
datetime.strptime(data.date, '%Y-%m-%d')
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
raise HTTPException(400, "Invalid date format. Use YYYY-MM-DD")
|
|
|
|
|
|
updates.append("date = %s")
|
|
|
|
|
|
values.append(data.date)
|
|
|
|
|
|
|
|
|
|
|
|
if data.rest_config:
|
|
|
|
|
|
updates.append("rest_config = %s")
|
2026-03-22 16:38:39 +01:00
|
|
|
|
values.append(Json(data.rest_config.model_dump()))
|
2026-03-22 17:36:49 +01:00
|
|
|
|
# Also update focus column if config changed
|
|
|
|
|
|
updates.append("focus = %s")
|
|
|
|
|
|
values.append(data.rest_config.focus)
|
feat: flexible rest days system with JSONB config (v9d Phase 2a)
PROBLEM: Simple full_rest/active_recovery model doesn't support
context-specific rest days (e.g., strength rest but cardio allowed).
SOLUTION: JSONB-based flexible rest day configuration.
## Changes:
**Migration 010:**
- Refactor rest_days.type → rest_config JSONB
- Schema: {focus, rest_from[], allows[], intensity_max}
- Validation function with check constraint
- GIN index for performant JSONB queries
**Backend (routers/rest_days.py):**
- CRUD: list, create (upsert by date), get, update, delete
- Stats: count per week, focus distribution
- Validation: check activity conflicts with rest day config
**Frontend (api.js):**
- 7 new methods: listRestDays, createRestDay, updateRestDay,
deleteRestDay, getRestDaysStats, validateActivity
**Integration:**
- Router registered in main.py
- Ready for weekly planning validation rules
## Next Steps:
- Frontend UI (RestDaysPage with Quick/Custom mode)
- Activity conflict warnings
- Dashboard widget
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 16:20:52 +01:00
|
|
|
|
|
|
|
|
|
|
if data.note is not None:
|
|
|
|
|
|
updates.append("note = %s")
|
|
|
|
|
|
values.append(data.note)
|
|
|
|
|
|
|
|
|
|
|
|
if not updates:
|
|
|
|
|
|
raise HTTPException(400, "No fields to update")
|
|
|
|
|
|
|
|
|
|
|
|
values.extend([rest_day_id, pid])
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
f"""
|
|
|
|
|
|
UPDATE rest_days
|
|
|
|
|
|
SET {', '.join(updates)}
|
|
|
|
|
|
WHERE id = %s AND profile_id = %s
|
|
|
|
|
|
RETURNING id, profile_id, date, rest_config, note, created_at
|
|
|
|
|
|
""",
|
|
|
|
|
|
values
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = cur.fetchone()
|
|
|
|
|
|
if not result:
|
|
|
|
|
|
raise HTTPException(404, "Rest day not found")
|
|
|
|
|
|
|
|
|
|
|
|
return r2d(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/{rest_day_id}")
|
|
|
|
|
|
def delete_rest_day(
|
|
|
|
|
|
rest_day_id: int,
|
|
|
|
|
|
x_profile_id: Optional[str] = Header(default=None),
|
|
|
|
|
|
session: dict = Depends(require_auth)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""Delete rest day."""
|
|
|
|
|
|
pid = get_pid(x_profile_id)
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"DELETE FROM rest_days WHERE id = %s AND profile_id = %s RETURNING id",
|
|
|
|
|
|
(rest_day_id, pid)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = cur.fetchone()
|
|
|
|
|
|
if not result:
|
|
|
|
|
|
raise HTTPException(404, "Rest day not found")
|
|
|
|
|
|
|
|
|
|
|
|
return {"deleted": True, "id": result['id']}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Stats & Validation ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/stats")
|
|
|
|
|
|
def get_rest_days_stats(
|
|
|
|
|
|
weeks: int = 4,
|
|
|
|
|
|
x_profile_id: Optional[str] = Header(default=None),
|
|
|
|
|
|
session: dict = Depends(require_auth)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""Get rest day statistics (count per week, focus distribution)."""
|
|
|
|
|
|
pid = get_pid(x_profile_id)
|
|
|
|
|
|
|
|
|
|
|
|
cutoff_date = (datetime.now() - timedelta(weeks=weeks)).strftime('%Y-%m-%d')
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
# Total count
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT COUNT(*) as total
|
|
|
|
|
|
FROM rest_days
|
|
|
|
|
|
WHERE profile_id = %s AND date >= %s
|
|
|
|
|
|
""",
|
|
|
|
|
|
(pid, cutoff_date)
|
|
|
|
|
|
)
|
|
|
|
|
|
total = cur.fetchone()['total']
|
|
|
|
|
|
|
|
|
|
|
|
# Count by focus type
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT
|
|
|
|
|
|
rest_config->>'focus' as focus,
|
|
|
|
|
|
COUNT(*) as count
|
|
|
|
|
|
FROM rest_days
|
|
|
|
|
|
WHERE profile_id = %s AND date >= %s
|
|
|
|
|
|
GROUP BY rest_config->>'focus'
|
|
|
|
|
|
ORDER BY count DESC
|
|
|
|
|
|
""",
|
|
|
|
|
|
(pid, cutoff_date)
|
|
|
|
|
|
)
|
|
|
|
|
|
by_focus = [r2d(r) for r in cur.fetchall()]
|
|
|
|
|
|
|
|
|
|
|
|
# Count by week (ISO week number)
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT
|
|
|
|
|
|
EXTRACT(YEAR FROM date) as year,
|
|
|
|
|
|
EXTRACT(WEEK FROM date) as week,
|
|
|
|
|
|
COUNT(*) as count
|
|
|
|
|
|
FROM rest_days
|
|
|
|
|
|
WHERE profile_id = %s AND date >= %s
|
|
|
|
|
|
GROUP BY year, week
|
|
|
|
|
|
ORDER BY year DESC, week DESC
|
|
|
|
|
|
""",
|
|
|
|
|
|
(pid, cutoff_date)
|
|
|
|
|
|
)
|
|
|
|
|
|
by_week = [r2d(r) for r in cur.fetchall()]
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"total_rest_days": total,
|
|
|
|
|
|
"weeks_analyzed": weeks,
|
|
|
|
|
|
"by_focus": by_focus,
|
|
|
|
|
|
"by_week": by_week
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/validate-activity")
|
|
|
|
|
|
def validate_activity(
|
|
|
|
|
|
data: ActivityConflictCheck,
|
|
|
|
|
|
x_profile_id: Optional[str] = Header(default=None),
|
|
|
|
|
|
session: dict = Depends(require_auth)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Check if activity conflicts with rest day configuration.
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
- conflict: bool
|
|
|
|
|
|
- severity: 'warning' | 'info' | 'none'
|
|
|
|
|
|
- message: str
|
|
|
|
|
|
"""
|
|
|
|
|
|
pid = get_pid(x_profile_id)
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rest_config
|
|
|
|
|
|
FROM rest_days
|
|
|
|
|
|
WHERE profile_id = %s AND date = %s
|
|
|
|
|
|
""",
|
|
|
|
|
|
(pid, data.date)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
row = cur.fetchone()
|
|
|
|
|
|
if not row:
|
|
|
|
|
|
return {"conflict": False, "severity": "none", "message": ""}
|
|
|
|
|
|
|
|
|
|
|
|
config = row['rest_config']
|
|
|
|
|
|
|
|
|
|
|
|
# Check if activity is in rest_from
|
|
|
|
|
|
if data.activity_type in config.get('rest_from', []):
|
|
|
|
|
|
focus_labels = {
|
|
|
|
|
|
'muscle_recovery': 'Muskelregeneration',
|
|
|
|
|
|
'cardio_recovery': 'Cardio-Erholung',
|
|
|
|
|
|
'mental_rest': 'Mentale Erholung',
|
|
|
|
|
|
'deload': 'Deload',
|
|
|
|
|
|
'injury': 'Verletzungspause'
|
|
|
|
|
|
}
|
|
|
|
|
|
focus_label = focus_labels.get(config.get('focus'), 'Ruhetag')
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"conflict": True,
|
|
|
|
|
|
"severity": "warning",
|
|
|
|
|
|
"message": f"Ruhetag ({focus_label}) – {data.activity_type} sollte pausiert werden. Trotzdem erfassen?"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Check if activity is allowed
|
|
|
|
|
|
allows_list = config.get('allows', [])
|
|
|
|
|
|
if allows_list and data.activity_type not in allows_list:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"conflict": True,
|
|
|
|
|
|
"severity": "info",
|
|
|
|
|
|
"message": f"Aktivität nicht in erlaubten Aktivitäten. Heute: {', '.join(allows_list) or 'Keine'}."
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Check intensity_max (if provided in request)
|
|
|
|
|
|
intensity_max = config.get('intensity_max')
|
|
|
|
|
|
if intensity_max:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"conflict": False,
|
|
|
|
|
|
"severity": "info",
|
|
|
|
|
|
"message": f"Erlaubt bei max. {intensity_max}% HFmax."
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {"conflict": False, "severity": "none", "message": ""}
|