Initial commit: smartrun MVP

Garmin run classification and progression tracker. Go backend (MCP
client to mcp-garmin, SQLite store, deterministic rule engine, REST
API) and React/TS frontend (Dashboard, Review Queue, Workout Kinds).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 18:33:06 +02:00
commit f689f74ae0
69 changed files with 10666 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
CREATE TABLE activities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
garmin_activity_id INTEGER NOT NULL UNIQUE,
activity_name TEXT NOT NULL DEFAULT '',
activity_type TEXT NOT NULL,
start_time_utc TEXT NOT NULL,
begin_timestamp_ms INTEGER NOT NULL,
duration_seconds REAL NOT NULL,
distance_meters REAL NOT NULL,
avg_hr REAL,
max_hr REAL,
avg_speed_mps REAL,
max_speed_mps REAL,
elevation_gain_m REAL,
elevation_loss_m REAL,
calories REAL,
lap_count INTEGER NOT NULL DEFAULT 0,
aerobic_training_effect REAL,
anaerobic_training_effect REAL,
training_effect_label TEXT NOT NULL DEFAULT '',
vo2max_value REAL,
hr_time_in_zone_1 REAL,
hr_time_in_zone_2 REAL,
hr_time_in_zone_3 REAL,
hr_time_in_zone_4 REAL,
hr_time_in_zone_5 REAL,
raw_json TEXT NOT NULL,
details_fetched_at TEXT,
details_raw_json TEXT,
splits_fetched_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_activities_start_time ON activities(start_time_utc);
CREATE INDEX idx_activities_activity_type ON activities(activity_type);
CREATE TABLE laps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
activity_id INTEGER NOT NULL REFERENCES activities(id) ON DELETE CASCADE,
lap_index INTEGER NOT NULL,
start_time_utc TEXT NOT NULL,
duration_seconds REAL NOT NULL,
distance_meters REAL NOT NULL,
avg_hr REAL,
max_hr REAL,
avg_speed_mps REAL,
max_speed_mps REAL,
elevation_gain_m REAL,
elevation_loss_m REAL,
intensity_type TEXT NOT NULL DEFAULT '',
hr_drift_bpm_per_min REAL,
hr_recovery_bpm_per_min REAL,
raw_json TEXT NOT NULL,
UNIQUE(activity_id, lap_index)
);
CREATE TABLE activity_samples (
id INTEGER PRIMARY KEY AUTOINCREMENT,
activity_id INTEGER NOT NULL REFERENCES activities(id) ON DELETE CASCADE,
elapsed_seconds REAL NOT NULL,
timestamp_ms INTEGER NOT NULL,
heart_rate REAL,
speed_mps REAL,
distance_m REAL,
elevation_m REAL
);
CREATE INDEX idx_activity_samples_activity ON activity_samples(activity_id, elapsed_seconds);
CREATE TABLE workout_kinds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT NOT NULL DEFAULT '',
color TEXT NOT NULL DEFAULT '',
rule_json TEXT NOT NULL,
priority INTEGER NOT NULL DEFAULT 0,
is_active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE kind_assignments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
activity_id INTEGER NOT NULL REFERENCES activities(id) ON DELETE CASCADE,
workout_kind_id INTEGER REFERENCES workout_kinds(id),
assignment_source TEXT NOT NULL CHECK(assignment_source IN ('rule_engine','manual')),
status TEXT NOT NULL CHECK(status IN ('assigned','needs_review')),
confidence REAL,
candidate_kinds_json TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_kind_assignments_activity ON kind_assignments(activity_id);
CREATE INDEX idx_kind_assignments_status ON kind_assignments(status);
CREATE VIEW current_kind_assignment AS
SELECT a.* FROM kind_assignments a
JOIN (
SELECT activity_id, MAX(id) AS max_id
FROM kind_assignments GROUP BY activity_id
) latest ON a.activity_id = latest.activity_id AND a.id = latest.max_id;
CREATE TABLE sync_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL CHECK(kind IN ('backfill','incremental')),
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
);

View File

@@ -0,0 +1,11 @@
-- Tracks how far back a full backfill has already reached. Garmin activity
-- history is immutable once recorded, so once we've backfilled a historical
-- window there is no need to ever re-fetch get_activities() for it again --
-- this is the "cache" that lets repeated backfills be cheap/fast instead of
-- re-walking years of history against Garmin's API every time.
CREATE TABLE sync_state (
id INTEGER PRIMARY KEY CHECK (id = 1),
earliest_synced_date TEXT,
backfill_complete INTEGER NOT NULL DEFAULT 0
);
INSERT INTO sync_state (id, earliest_synced_date, backfill_complete) VALUES (1, NULL, 0);