From b65efd3b7181fc9d482f7d2e04989b4905547273 Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 22 Mar 2026 10:59:55 +0100 Subject: [PATCH] feat: add missing migration 008 (vitals, rest days, sleep_goal_minutes) - Creates rest_days table for rest day tracking - Creates vitals_log table for resting HR + HRV - Creates weekly_goals table for training planning - Extends profiles with hf_max and sleep_goal_minutes columns - Extends activity_log with avg_hr and max_hr columns - Fixes sleep_goal_minutes missing column error in stats endpoint - Includes stats error handling in SleepWidget Co-Authored-By: Claude Opus 4.6 --- backend/migrations/008_vitals_rest_days.sql | 59 +++++++++++++++++++++ frontend/src/components/SleepWidget.jsx | 2 +- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 backend/migrations/008_vitals_rest_days.sql diff --git a/backend/migrations/008_vitals_rest_days.sql b/backend/migrations/008_vitals_rest_days.sql new file mode 100644 index 0000000..80dbfdb --- /dev/null +++ b/backend/migrations/008_vitals_rest_days.sql @@ -0,0 +1,59 @@ +-- Migration 008: Vitals, Rest Days, Weekly Goals +-- v9d Phase 2: Sleep & Vitals Module +-- Date: 2026-03-22 + +-- Rest Days +CREATE TABLE IF NOT EXISTS rest_days ( + id SERIAL PRIMARY KEY, + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + date DATE NOT NULL, + type VARCHAR(20) NOT NULL CHECK (type IN ('full_rest', 'active_recovery')), + note TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT unique_rest_day_per_profile UNIQUE(profile_id, date) +); +CREATE INDEX idx_rest_days_profile_date ON rest_days(profile_id, date DESC); + +-- Vitals (Resting HR + HRV) +CREATE TABLE IF NOT EXISTS vitals_log ( + id SERIAL PRIMARY KEY, + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + date DATE NOT NULL, + resting_hr INTEGER CHECK (resting_hr > 0 AND resting_hr < 200), + hrv INTEGER CHECK (hrv > 0), + note TEXT, + source VARCHAR(20) DEFAULT 'manual' CHECK (source IN ('manual', 'apple_health', 'garmin')), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT unique_vitals_per_day UNIQUE(profile_id, date) +); +CREATE INDEX idx_vitals_profile_date ON vitals_log(profile_id, date DESC); + +-- Extend activity_log for heart rate data +ALTER TABLE activity_log +ADD COLUMN IF NOT EXISTS avg_hr INTEGER CHECK (avg_hr > 0 AND avg_hr < 250), +ADD COLUMN IF NOT EXISTS max_hr INTEGER CHECK (max_hr > 0 AND max_hr < 250); + +-- Extend profiles for HF max and sleep goal +ALTER TABLE profiles +ADD COLUMN IF NOT EXISTS hf_max INTEGER CHECK (hf_max > 0 AND hf_max < 250), +ADD COLUMN IF NOT EXISTS sleep_goal_minutes INTEGER DEFAULT 450 CHECK (sleep_goal_minutes > 0); + +-- Weekly Goals (Soll/Ist Wochenplanung) +CREATE TABLE IF NOT EXISTS weekly_goals ( + id SERIAL PRIMARY KEY, + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + week_start DATE NOT NULL, + goals JSONB NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT unique_weekly_goal_per_profile UNIQUE(profile_id, week_start) +); +CREATE INDEX idx_weekly_goals_profile_week ON weekly_goals(profile_id, week_start DESC); + +-- Comments for documentation +COMMENT ON TABLE rest_days IS 'v9d Phase 2: Rest days tracking (full rest or active recovery)'; +COMMENT ON TABLE vitals_log IS 'v9d Phase 2: Daily vitals (resting HR, HRV)'; +COMMENT ON TABLE weekly_goals IS 'v9d Phase 2: Weekly training goals (Soll/Ist planning)'; +COMMENT ON COLUMN profiles.hf_max IS 'Maximum heart rate for HR zone calculation'; +COMMENT ON COLUMN profiles.sleep_goal_minutes IS 'Sleep goal in minutes (default: 450 = 7h 30min)'; diff --git a/frontend/src/components/SleepWidget.jsx b/frontend/src/components/SleepWidget.jsx index a6353f3..32cb323 100644 --- a/frontend/src/components/SleepWidget.jsx +++ b/frontend/src/components/SleepWidget.jsx @@ -24,7 +24,7 @@ export default function SleepWidget() { const load = () => { Promise.all([ api.listSleep(1), // Get last entry - api.getSleepStats(7) + api.getSleepStats(7).catch(() => null) // Stats optional ]).then(([sleepData, statsData]) => { setLastNight(sleepData[0] || null) setStats(statsData)