Production Release: RestDays Widget + Trainingstyp Fix #16

Merged
Lars merged 10 commits from develop into main 2026-03-23 09:24:17 +01:00
2 changed files with 67 additions and 13 deletions
Showing only changes of commit f87b93ce2f - Show all commits

View File

@ -0,0 +1,34 @@
-- Migration 012: Unique constraint on (profile_id, date, focus)
-- v9d Phase 2a: Prevent duplicate rest day types per date
-- Date: 2026-03-22
-- Add focus column (extracted from rest_config for performance + constraints)
ALTER TABLE rest_days
ADD COLUMN IF NOT EXISTS focus VARCHAR(20);
-- Populate from existing JSONB data
UPDATE rest_days
SET focus = rest_config->>'focus'
WHERE focus IS NULL;
-- Make NOT NULL (safe because we just populated all rows)
ALTER TABLE rest_days
ALTER COLUMN focus SET NOT NULL;
-- Add CHECK constraint for valid focus values
ALTER TABLE rest_days
ADD CONSTRAINT valid_focus CHECK (
focus IN ('muscle_recovery', 'cardio_recovery', 'mental_rest', 'deload', 'injury')
);
-- Add UNIQUE constraint: Same profile + date + focus = duplicate
ALTER TABLE rest_days
ADD CONSTRAINT unique_rest_day_per_focus
UNIQUE (profile_id, date, focus);
-- Add index for efficient queries by focus
CREATE INDEX IF NOT EXISTS idx_rest_days_focus
ON rest_days(focus);
-- Comment for documentation
COMMENT ON COLUMN rest_days.focus IS 'Extracted from rest_config.focus for performance and constraints. Prevents duplicate rest day types per date.';

View File

@ -10,6 +10,7 @@ from datetime import datetime, timedelta
from fastapi import APIRouter, HTTPException, Depends, Header from fastapi import APIRouter, HTTPException, Depends, Header
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from psycopg2.extras import Json from psycopg2.extras import Json
from psycopg2.errors import UniqueViolation
from db import get_db, get_cursor, r2d from db import get_db, get_cursor, r2d
from auth import require_auth from auth import require_auth
@ -89,22 +90,38 @@ def create_rest_day(
# Convert RestConfig to dict for JSONB storage # Convert RestConfig to dict for JSONB storage
config_dict = data.rest_config.model_dump() config_dict = data.rest_config.model_dump()
focus = data.rest_config.focus
try:
with get_db() as conn: with get_db() as conn:
cur = get_cursor(conn) cur = get_cursor(conn)
# Insert (multiple entries per date allowed) # Insert (multiple entries per date allowed, but not same focus)
cur.execute( cur.execute(
""" """
INSERT INTO rest_days (profile_id, date, rest_config, note, created_at) INSERT INTO rest_days (profile_id, date, focus, rest_config, note, created_at)
VALUES (%s, %s, %s, %s, CURRENT_TIMESTAMP) VALUES (%s, %s, %s, %s, %s, CURRENT_TIMESTAMP)
RETURNING id, profile_id, date, rest_config, note, created_at RETURNING id, profile_id, date, focus, rest_config, note, created_at
""", """,
(pid, data.date, Json(config_dict), data.note) (pid, data.date, focus, Json(config_dict), data.note)
) )
result = cur.fetchone() result = cur.fetchone()
return r2d(result) 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."
)
@router.get("/{rest_day_id}") @router.get("/{rest_day_id}")
@ -159,6 +176,9 @@ def update_rest_day(
if data.rest_config: if data.rest_config:
updates.append("rest_config = %s") updates.append("rest_config = %s")
values.append(Json(data.rest_config.model_dump())) values.append(Json(data.rest_config.model_dump()))
# Also update focus column if config changed
updates.append("focus = %s")
values.append(data.rest_config.focus)
if data.note is not None: if data.note is not None:
updates.append("note = %s") updates.append("note = %s")