Problem: Import expected English column names, but German Apple Health/Omron
exports use German names with units.
Fixed:
- Apple Health: Support both English and German column names
- "Start" OR "Datum/Uhrzeit"
- "Resting Heart Rate" OR "Ruhepuls (count/min)"
- "Heart Rate Variability" OR "Herzfrequenzvariabilität (ms)"
- "VO2 Max" OR "VO2 max (ml/(kg·min))"
- "Oxygen Saturation" OR "Blutsauerstoffsättigung (%)"
- "Respiratory Rate" OR "Atemfrequenz (count/min)"
- Omron: Support column names with/without units
- "Systolisch (mmHg)" OR "Systolisch"
- "Diastolisch (mmHg)" OR "Diastolisch"
- "Puls (bpm)" OR "Puls"
- "Unregelmäßiger Herzschlag festgestellt" OR "Unregelmäßiger Herzschlag"
- "Mögliches AFib" OR "Vorhofflimmern"
Added debug logging for both imports to show detected columns.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Logs:
- CSV column names from first row
- Rows skipped due to missing date
- Rows skipped due to no vitals data
- Shows which fields were found/missing
Helps diagnose CSV format mismatches.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Problem: Import reported all entries as "updated" even when skipped
due to WHERE clause (source != 'manual')
Root cause: RETURNING returns NULL when WHERE clause prevents update,
but code counted NULL as "updated" instead of "skipped"
Fix:
- Check if result is None → skipped (WHERE prevented update)
- Check if xmax = 0 → inserted (new row)
- Otherwise → updated (existing row modified)
Affects:
- vitals_baseline.py: Apple Health import
- blood_pressure.py: Omron import
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
**Backend (insights.py):**
- Extended _get_profile_data() to fetch sleep, rest_days, vitals
- Added template variables for Sleep Module:
{{sleep_summary}}, {{sleep_detail}}, {{sleep_avg_duration}}, {{sleep_avg_quality}}
- Added template variables for Rest Days:
{{rest_days_summary}}, {{rest_days_count}}, {{rest_days_types}}
- Added template variables for Vitals:
{{vitals_summary}}, {{vitals_detail}}, {{vitals_avg_hr}}, {{vitals_avg_hrv}},
{{vitals_avg_bp}}, {{vitals_vo2_max}}
**Frontend (Analysis.jsx):**
- Added 12 new template variables to VARS list in PromptEditor
- Enables AI prompt creation for Sleep, Rest Days, and Vitals analysis
All modules now have AI evaluation support for future prompt creation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Import endpoints for Omron blood pressure CSV (German date format)
- Import endpoints for Apple Health vitals CSV
- Import UI tab in VitalsPage with drag & drop for both sources
- German month mapping for Omron date parsing ("13 März 2026")
- Upsert logic preserves manual entries (source != 'manual')
- Import result feedback (inserted/updated/skipped/errors)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Avg blood pressure (systolic/diastolic) 7d and 30d
- Latest VO2 Max value
- Avg SpO2 7d and 30d
- Backend now provides all metrics expected by frontend
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migration 014:
- blood_pressure_systolic/diastolic (mmHg)
- pulse (bpm) - during BP measurement
- vo2_max (ml/kg/min) - from Apple Watch
- spo2 (%) - blood oxygen saturation
- respiratory_rate (breaths/min)
- irregular_heartbeat, possible_afib (boolean flags from Omron)
- Added 'omron' to source enum
Backend:
- Updated Pydantic models (VitalsEntry, VitalsUpdate)
- Updated all SELECT queries to include new fields
- Updated INSERT/UPDATE with COALESCE for partial updates
- Validation: at least one vital must be provided
Preparation for Omron + Apple Health imports
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend:
- New router: vitals.py with CRUD endpoints
- GET /api/vitals (list)
- GET /api/vitals/by-date/{date}
- POST /api/vitals (upsert)
- PUT /api/vitals/{id}
- DELETE /api/vitals/{id}
- GET /api/vitals/stats (7d/30d averages, trends)
- Registered in main.py
Frontend:
- VitalsPage.jsx with manual entry form
- List with inline editing
- Stats overview (averages, trend indicators)
- Added to CaptureHub (❤️ icon)
- Route /vitals in App.jsx
API:
- Added vitals methods to api.js
v9d Phase 2d - Vitals tracking complete
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Admin endpoints for profile configuration:
- Extended TrainingTypeCreate/Update models with profile field
- Added profile column to all SELECT queries
- Profile templates for Running, Meditation, Strength Training
- Template endpoints: list, get, apply
- Profile stats endpoint (configured/unconfigured count)
New file: profile_templates.py
- TEMPLATE_RUNNING: Endurance-focused with HR zones
- TEMPLATE_MEDITATION: Mental-focused (low HR ≤ instead of ≥)
- TEMPLATE_STRENGTH: Strength-focused
API Endpoints:
- GET /api/admin/training-types/profiles/templates
- GET /api/admin/training-types/profiles/templates/{key}
- POST /api/admin/training-types/{id}/profile/apply-template
- GET /api/admin/training-types/profiles/stats
Next: Frontend Admin-UI (ProfileEditor component)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Problem: Backend crashed on startup due to evaluation import failure
Solution: Wrap evaluation_helper import in try/except
Changes:
- Import evaluation_helper with error handling
- Add EVALUATION_AVAILABLE flag
- All evaluation calls now check flag before executing
- System remains functional even if evaluation system unavailable
This prevents backend crashes if:
- Migrations haven't run yet
- Dependencies are missing
- Import errors occur
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Automatic evaluation on activity INSERT/UPDATE:
- create_activity(): Evaluate after manual creation
- update_activity(): Re-evaluate after manual update
- import_activity_csv(): Evaluate after CSV import (INSERT + UPDATE)
- bulk_categorize_activities(): Evaluate after bulk training type assignment
All evaluation calls wrapped in try/except to prevent activity operations
from failing if evaluation encounters an error. Only activities with
training_type_id assigned are evaluated.
Phase 1.2 complete ✅
## Next Steps (Phase 2):
Admin-UI for training type profile configuration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Problem: Creating new training types via Admin UI resulted in
'Internal Server Error' because abilities dict was passed directly
to PostgreSQL JSONB column without Json() wrapper.
Solution:
- Import Json from psycopg2.extras
- Wrap abilities_json with Json() in INSERT
- Wrap data.abilities with Json() in UPDATE
Same issue as rest_days JSONB fix (commit 7d627cf).
Closes#13
Problem: User can create multiple rest days of same type per date
(e.g., 2x Mental Rest on 2026-03-23) - makes no sense.
Solution: UNIQUE constraint on (profile_id, date, focus)
## Migration 012:
- Add focus column (extracted from rest_config JSONB)
- Populate from existing data
- Add NOT NULL constraint
- Add CHECK constraint (valid focus values)
- Add UNIQUE constraint (profile_id, date, focus)
- Add index for performance
## Backend:
- Insert focus column alongside rest_config
- Handle UniqueViolation gracefully
- User-friendly error: "Du hast bereits einen Ruhetag 'Muskelregeneration' für 23.03."
## Benefits:
- DB-level enforcement (clean)
- Fast queries (no JSONB scan)
- Clear error messages
- Prevents: 2x muscle_recovery same day
- Allows: muscle_recovery + mental_rest same day ✓
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migration 011 removed UNIQUE constraint (profile_id, date) to allow
multiple rest days per date, but INSERT still used ON CONFLICT.
Error: psycopg2.errors.InvalidColumnReference: there is no unique or
exclusion constraint matching the ON CONFLICT specification
Solution: Remove ON CONFLICT clause, use plain INSERT.
Multiple entries per date now allowed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Error: psycopg2.ProgrammingError: can't adapt type 'dict'
Solution: Import psycopg2.extras.Json and wrap config_dict
Changes:
- Import Json from psycopg2.extras
- Wrap config_dict with Json() in INSERT
- Wrap config_dict with Json() in UPDATE
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Problem: Photos were always getting NULL date instead of form date,
causing frontend to fallback to created timestamp (today).
Root cause: FastAPI requires Form() wrapper for form fields when
mixing with File() parameters. Without it, the date parameter was
treated as query parameter and always received empty string.
Solution:
- Import Form from fastapi
- Change date parameter from str="" to str=Form("")
- Return photo_date instead of date in response (consistency)
Now photos correctly use the date from the upload form and can be
backdated when uploading later.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Problem:
- Photo upload with empty date parameter (date='')
- PostgreSQL rejects empty string for DATE field
- Error: "invalid input syntax for type date: ''"
- Occurred when saving circumference entry with only photo
Fix:
- Convert empty string to NULL before INSERT
- Check: date if date and date.strip() else None
- NULL is valid for optional date field
Test case:
- Circumference entry with only photo → should work now
- Photo without date → stored with date=NULL ✓
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Conceptual change: duration_minutes = actual sleep time (not time in bed)
Backend:
- Plausibility check: deep + rem + light = duration (awake separate)
- Import: duration = deep + rem + light (without awake)
- Updated error message: clarifies awake not counted
Frontend:
- Label: "Schlafdauer (reine Schlafzeit, Minuten)"
- Auto-calculate: bedtime-waketime minus awake_minutes
- Plausibility check: only validates sleep phases (not awake)
- Both NewEntry and Edit mode updated
Rationale:
- Standard in sleep tracking (Apple Health shows "Sleep", not "Time in Bed")
- Clearer semantics: duration = how long you slept
- awake_minutes tracked separately for analysis
- More intuitive for users
Example:
- Time in bed: 22:00 - 06:00 = 480 min (8h)
- Awake phases: 30 min
- Sleep duration: 450 min (7h 30min) ✓
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Problem: Segments crossing midnight were split into different nights
- 22:30-23:15 (21.03) → assigned to 21.03
- 00:30-02:45 (22.03) → assigned to 22.03
But both belong to the same night (21/22.03)!
Solution: Gap-based grouping
- Sort segments chronologically
- Group segments with gap < 2 hours
- Night date = wake_time.date() (last segment's end date)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend:
- New endpoint POST /api/sleep/import/apple-health
- Parses Apple Health sleep CSV format
- Maps German phase names (Kern→light, REM→rem, Tief→deep, Wach→awake)
- Aggregates segments by night (wake date)
- Stores raw segments in JSONB (sleep_segments)
- Does NOT overwrite manual entries (source='manual')
Frontend:
- Import button in SleepPage with file picker
- Progress indicator during import
- Success/error messages
- Auto-refresh after import
Documentation:
- Added architecture rules reference to CLAUDE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PostgreSQL TIME type doesn't accept empty strings.
Converting empty bedtime/wake_time to None before INSERT/UPDATE.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add sleep_log table with JSONB sleep_segments (Migration 009)
- Add sleep router with CRUD + stats endpoints (7d avg, 14d debt, trend, phases)
- Add SleepPage with quick/detail entry forms and inline edit
- Add SleepWidget to Dashboard showing last night + 7d average
- Add sleep navigation entry with Moon icon
- Register sleep router in main.py
- Add 9 new API methods in api.js
Phase 2b complete - ready for testing on dev
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Issue 1: Automatic training type mapping didn't work
- Root cause: Only English workout names were mapped
- Solution: Added 20+ German workout type mappings:
- "Traditionelles Krafttraining" → hypertrophy
- "Outdoor Spaziergang" → walk
- "Innenräume Spaziergang" → walk
- "Matrial Arts" → technique (handles typo)
- "Cardio Dance" → dance
- "Geist & Körper" → yoga
- Plus: Laufen, Gehen, Radfahren, Schwimmen, etc.
Issue 2: Reimporting CSV created duplicates without training types
- Root cause: Import always did INSERT with new UUID, no duplicate check
- Solution: Check if entry exists (profile_id + date + start_time)
- If exists: UPDATE with new data + training type mapping
- If new: INSERT as before
- Handles multiple workouts per day (different start times)
- "Skipped" count now includes updated entries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend (v9d Phase 1b):
- Migration 006: Add abilities JSONB column + descriptions
- admin_training_types.py: Full CRUD endpoints for training types
- List, Get, Create, Update, Delete
- Abilities taxonomy endpoint (5 dimensions: koordinativ, konditionell, kognitiv, psychisch, taktisch)
- Validation: Cannot delete types in use
- Register admin_training_types router in main.py
Frontend:
- AdminTrainingTypesPage: Full CRUD UI
- Create/edit form with all fields (category, subcategory, names, icon, descriptions, sort_order)
- List grouped by category with color coding
- Delete with usage check
- Note about abilities mapping coming in v9f
- Add TrainingTypeDistribution to ActivityPage stats tab
- Add admin link in AdminPanel (v9d section)
- Update api.js with admin training types methods
Notes:
- Abilities mapping UI deferred to v9f (flexible prompt system)
- Placeholders (abilities column) in place for future AI analysis
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Migration 005: Add cardio subcategories (Gehen, Tanzen)
- Migration 005: Add new category "Geist & Meditation" with 4 subcategories
(Meditation, Atemarbeit, Achtsamkeit, Visualisierung)
- Update categories endpoint with mind category metadata
- Update Apple Health mapping: dance → dance, add meditation/mindfulness
- 6 new training types total
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add get_training_type_for_apple_health() mapping function (23 workout types)
- CSV import now automatically assigns training_type_id/category/subcategory
- New endpoint: GET /activity/uncategorized (grouped by activity_type)
- New endpoint: POST /activity/bulk-categorize (bulk update training types)
- New component: BulkCategorize with two-level dropdown selection
- ActivityPage: new "Kategorisieren" tab for existing activities
- Update CLAUDE.md: v9d Phase 1b progress
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1: Training Types Basis
=============================
Backend:
- Migration 004: training_types table + seed data (24 types)
- New router: /api/training-types (grouped, flat, categories)
- Extend activity_log: training_type_id, training_category, training_subcategory
- Extend ActivityEntry model: support training type fields
Frontend:
- TrainingTypeSelect component (two-level dropdown)
- TrainingTypeDistribution component (pie chart)
- API functions: listTrainingTypes, listTrainingTypesFlat, getTrainingCategories
Quick Win: Logout Button
========================
- Add LogOut icon button in app header
- Confirm dialog before logout
- Redirect to / after logout
- Hover effect: red color on hover
Not yet integrated:
- TrainingTypeSelect not yet in ActivityPage form
- TrainingTypeDistribution not yet in Dashboard
(will be added in next commit)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Use window.location.href instead of navigate() for reliable redirect
2. Improve backend error message for already-used verification tokens
3. Show user-friendly message when token was already verified
4. Reduce redirect delay from 2s to 1.5s for better UX
Fixes:
- Empty page after email verification
- Generic error when clicking verification link twice
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend fixes:
- Fixed timezone-aware datetime comparison in verify_email endpoint
- Added trial_ends_at (14 days) for new registrations
- All datetime.now() calls now use timezone.utc
Frontend additions:
- Added EmailVerificationBanner component for unverified users
- Banner shows warning before trial banner in Dashboard
- Clear messaging about verification requirement
This fixes the 500 error on email verification and ensures new users
see both verification and trial status correctly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend:
- New endpoint: POST /api/auth/register
- New endpoint: GET /api/auth/verify/{token}
- Migration: Add email_verified, verification_token, verification_expires
- Helper: send_email() for reusable SMTP
- Validation: email format, password length (min 8), name
- Auto-login after verification (returns session token)
- Rate limit: 3 registrations per hour per IP
Features:
- Verification token valid for 24h
- Existing users marked as verified (grandfather clause)
- SMTP configured via .env (SMTP_HOST, SMTP_USER, SMTP_PASS)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Features:
- Manual entry form above data list
- Date picker with auto-load existing entries
- Upsert logic: creates new or updates existing entry
- Smart button text: "Hinzufügen" vs "Aktualisieren"
- Prevents duplicate entries per day
- Feature enforcement for nutrition_entries
Backend:
- POST /nutrition - Create or update entry (upsert)
- GET /nutrition/by-date/{date} - Load entry by date
- Auto-detects existing entry and switches to UPDATE mode
- Increments usage counter only on INSERT
Frontend:
- EntryForm component with date picker + macros inputs
- Auto-loads data when date changes
- Shows info message when entry exists
- Success/error feedback
- Disabled state while loading/saving
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Features:
- Import history panel showing all CSV imports with date, count, and range
- Edit/delete functionality for nutrition entries (inline editing)
- New backend endpoints: GET /import-history, PUT /{id}, DELETE /{id}
UI Changes:
- Import history displayed under import panel
- "Daten" tab now has edit/delete buttons per entry
- Inline form for editing macros (kcal, protein, fat, carbs)
- Confirmation dialog for deletion
Backend:
- nutrition.py: Added import_history, update_nutrition, delete_nutrition endpoints
- Groups imports by created date to show history
Frontend:
- NutritionPage: New DataTab and ImportHistory components
- api.js: Added nutritionImportHistory, updateNutrition, deleteNutrition
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Problem:
- /api/nutrition/weekly crashed with 500 Internal Server Error
- TypeError: strptime() argument 1 must be str, not datetime.date
Root Cause:
- d['date'] from PostgreSQL is already datetime.date object
- datetime.strptime() expects string input
- Line 156: wk=datetime.strptime(d['date'],'%Y-%m-%d').strftime('%Y-W%V')
Solution:
- Added type check before strptime()
- If date already has strftime method → use directly
- Else → parse as string first
- Works with both datetime.date objects and strings
Tested:
- /nutrition page loads without error
- Weekly aggregation works correctly
- Chart displays nutrition data
Closes: BUG-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add user-facing usage overview endpoint
- Returns all features with usage, limits, reset info
- Fully dynamic - automatically includes new features
- Phase 3: Frontend Display preparation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create feature_logger.py with JSON logging infrastructure
- Add log_feature_usage() calls to all 9 routers after check_feature_access()
- Logs written to /app/logs/feature-usage.log
- Tracks all usage (not just violations) for future analysis
- Phase 2: Non-blocking monitoring complete
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes two critical bugs in AI analysis storage:
1. History now works - analyses are saved, not overwritten
- Removed DELETE statements before INSERT in insights.py
- All analyses are now preserved per scope
- Displayed in descending order by creation date
2. Pipeline saves under correct scope 'pipeline' instead of 'gesamt'
- Changed scope from 'gesamt' to 'pipeline' in pipeline endpoint
- Pipeline results now appear under correct category in history
3. Fixed pipeline appearing twice in UI
- Filter now excludes both 'pipeline_*' and 'pipeline' from individual list
- Pipeline only appears in dedicated section at top
Changes:
- backend/routers/insights.py: Removed DELETE, changed scope to 'pipeline'
- frontend/src/pages/Analysis.jsx: Fixed filter to exclude 'pipeline'
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>