Dedup Garmin data storage, configurable chart colors, taxonomy fixes, sync/progression fixes

- Remove duplicated Garmin fields from storage; decode display-only fields
  (activity name/type, lap duration/HR, structured workout raw JSON) from
  RawJSON at API-response time instead of storing redundant columns
- Add a fully configurable chart color system (pace/HR main-line colors, 4
  effort-kind colors, tint/darken/brighten intensity knobs) under Profile >
  Chart colors
- Rename training types and fix their display order (Easy, Long, 60'/30'
  Threshold, Tempo, Intervals, MAS Test, Race) everywhere they're listed
- Add an Efficiency Factor progression metric; fix Progression chart axes to
  use tight non-zero-based domains, m:ss/km pace formatting, and rounded
  ticks instead of raw floating-point labels
- Expose the raw get_workout_by_id() payload in the raw-data viewer
  alongside activity/lap/detail JSON; enlarge the modal and shrink array
  indentation for readability
- Fix "last sync" reporting a meaningless activity count: record one
  combined sync run per manual "Sync now" and count genuinely new
  activities instead of re-listing whatever Garmin returned for the queried
  window
- Let a Review Queue activity be manually cleared back to Unclassified, and
  make "Reset all" available even while disconnected from Garmin

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 06:33:47 +02:00
parent 9818d35910
commit 518bccf5ab
56 changed files with 2334 additions and 890 deletions

View File

@@ -0,0 +1,3 @@
-- Names the profile so a future multi-profile setup can show which one is
-- active. Only one profile row exists today (id=1), so this is just a label.
ALTER TABLE profile ADD COLUMN name TEXT NOT NULL DEFAULT 'Default';

View File

@@ -0,0 +1,6 @@
-- Replaces the named-zone-only expected_hr_zone with a custom %HRR range
-- per training type (e.g. "easy runs at 70-80% heart rate reserve"),
-- matching how pace range is already modeled. expected_hr_zone is left in
-- place but unused going forward -- nothing reads or writes it anymore.
ALTER TABLE workout_type_paces ADD COLUMN hr_min_pct_hrr REAL;
ALTER TABLE workout_type_paces ADD COLUMN hr_max_pct_hrr REAL;

View File

@@ -0,0 +1,35 @@
-- Phase 1 of the raw-JSON duplication cleanup: drops every Activity/Lap
-- column that was a pure untransformed copy of a value already present in
-- that row's own raw_json, with zero SQL/classify/functional consumer
-- anywhere in the app (see the 2026-07 field-by-field duplication audit).
-- Unlike this project's usual additive-only migrations, dropping these
-- columns outright is the whole point of this one -- leaving them inert
-- would keep the exact duplication being removed. activity_name/
-- activity_type (Activity) and duration_seconds/avg_hr (laps) also go here
-- even though the frontend still displays them: they're now decoded from
-- raw_json at API-response time instead of stored separately (see
-- internal/api's decodeActivityDisplayFields/decodeLapDisplayFields).
DROP INDEX idx_activities_activity_type;
ALTER TABLE activities DROP COLUMN activity_name;
ALTER TABLE activities DROP COLUMN activity_type;
ALTER TABLE activities DROP COLUMN begin_timestamp_ms;
ALTER TABLE activities DROP COLUMN max_speed_mps;
ALTER TABLE activities DROP COLUMN elevation_loss_m;
ALTER TABLE activities DROP COLUMN calories;
ALTER TABLE activities DROP COLUMN lap_count;
ALTER TABLE activities DROP COLUMN training_effect_label;
ALTER TABLE activities DROP COLUMN hr_time_in_zone_1;
ALTER TABLE activities DROP COLUMN hr_time_in_zone_2;
ALTER TABLE activities DROP COLUMN hr_time_in_zone_3;
ALTER TABLE activities DROP COLUMN hr_time_in_zone_4;
ALTER TABLE activities DROP COLUMN hr_time_in_zone_5;
ALTER TABLE laps DROP COLUMN start_time_utc;
ALTER TABLE laps DROP COLUMN duration_seconds;
ALTER TABLE laps DROP COLUMN distance_meters;
ALTER TABLE laps DROP COLUMN avg_hr;
ALTER TABLE laps DROP COLUMN max_hr;
ALTER TABLE laps DROP COLUMN max_speed_mps;
ALTER TABLE laps DROP COLUMN elevation_gain_m;
ALTER TABLE laps DROP COLUMN elevation_loss_m;

View File

@@ -0,0 +1,10 @@
-- User-configurable chart colors: 2 "main line" colors (one per metric) and
-- 4 "effort kind" colors (one per workout phase), used together to derive
-- the pace/HR chart's line, under-the-line fill, and phase-background fill
-- colors (see frontend's ExpectedVsActualChart).
ALTER TABLE profile ADD COLUMN pace_color TEXT NOT NULL DEFAULT '#3b82f6';
ALTER TABLE profile ADD COLUMN heart_rate_color TEXT NOT NULL DEFAULT '#ef4444';
ALTER TABLE profile ADD COLUMN warmup_color TEXT NOT NULL DEFAULT '#c2410c';
ALTER TABLE profile ADD COLUMN effort_color TEXT NOT NULL DEFAULT '#7c3aed';
ALTER TABLE profile ADD COLUMN recovery_color TEXT NOT NULL DEFAULT '#15803d';
ALTER TABLE profile ADD COLUMN cooldown_color TEXT NOT NULL DEFAULT '#fb923c';

View File

@@ -0,0 +1,18 @@
-- Renames the default training-type taxonomy to a fixed display convention
-- (drop the redundant "Run" suffix, numeral before "Threshold", "Intervals"
-- not "Interval") and assigns explicit priorities so kinds always list in
-- this exact order wherever they're shown (Activities filters, Progression's
-- kind picker, Profile's Training types card) -- existing ORDER BY priority
-- DESC, name already does the sorting, no query changes needed:
-- Easy, Long, 60' Threshold, 30' Threshold, Tempo, Intervals, MAS Test, Race.
--
-- Each UPDATE matches on the original seeded name, so a kind the user has
-- already renamed themselves (no longer matching) is left untouched.
UPDATE workout_kinds SET name = 'Easy', priority = 80 WHERE name = 'Easy Run';
UPDATE workout_kinds SET name = 'Long', priority = 70 WHERE name = 'Long Run';
UPDATE workout_kinds SET name = '60'' Threshold', priority = 60 WHERE name = 'Threshold 60''';
UPDATE workout_kinds SET name = '30'' Threshold', priority = 50 WHERE name = 'Threshold 30''';
UPDATE workout_kinds SET priority = 40 WHERE name = 'Tempo';
UPDATE workout_kinds SET name = 'Intervals', priority = 30 WHERE name = 'Interval';
UPDATE workout_kinds SET priority = 20 WHERE name = 'MAS Test';
UPDATE workout_kinds SET priority = 10 WHERE name = 'Race';

View File

@@ -0,0 +1,5 @@
-- How strongly the main line color (blue/red) tints the effort-kind fill
-- under the line, as a percentage (0-100) mixed in -- see frontend's
-- ExpectedVsActualChart mixColor(). The phase background above the line is
-- never tinted by the main line color regardless of this setting.
ALTER TABLE profile ADD COLUMN main_line_tint_pct REAL NOT NULL DEFAULT 20;

View File

@@ -0,0 +1,5 @@
-- How strongly (0-100) the effort-kind color is darkened for the phase
-- background above the line -- see frontend's ExpectedVsActualChart
-- darken(). Never mixed with the main line color, unlike the fill below the
-- line (see main_line_tint_pct).
ALTER TABLE profile ADD COLUMN background_darken_pct REAL NOT NULL DEFAULT 35;

View File

@@ -0,0 +1,5 @@
-- How strongly (0-100) a chart's main line color (and everything tinted
-- from it) is brightened when that chart actually has a structured-workout
-- target range to show -- a color-based cue that a target is present,
-- replacing a text label -- see frontend's ExpectedVsActualChart brighten().
ALTER TABLE profile ADD COLUMN target_brighten_pct REAL NOT NULL DEFAULT 20;

View File

@@ -0,0 +1,5 @@
-- Genuine raw JSON of the activity's structured Garmin workout (get_workout_by_id),
-- the source used to compute each lap's TargetPaceLowMps/HighMps and
-- TargetHRLowBpm/HighBpm (see internal/sync/mapping.go's alignWorkoutTargets).
-- Null for activities with no WorkoutID, or synced before this column existed.
ALTER TABLE activities ADD COLUMN workout_raw_json TEXT;

View File

@@ -0,0 +1,19 @@
-- Widens sync_runs.kind's CHECK constraint to also allow 'full' (a manual
-- "Sync now" pass recorded as one combined run instead of separate
-- backfill/incremental rows -- see internal/sync.Service.FullSync). SQLite
-- has no ALTER TABLE for CHECK constraints, so the table is rebuilt.
CREATE TABLE sync_runs_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL CHECK(kind IN ('backfill','incremental','full')),
started_at TEXT NOT NULL,
finished_at TEXT,
activities_fetched INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL CHECK(status IN ('running','success','error')),
error_message TEXT
);
INSERT INTO sync_runs_new (id, kind, started_at, finished_at, activities_fetched, status, error_message)
SELECT id, kind, started_at, finished_at, activities_fetched, status, error_message FROM sync_runs;
DROP TABLE sync_runs;
ALTER TABLE sync_runs_new RENAME TO sync_runs;