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,86 @@
import { useEffect, useState } from "react";
import { api } from "../api/client";
import { ProgressionChart } from "../components/charts/ProgressionChart";
import type { ProgressionMetric, ProgressionPoint, WorkoutKind } from "../types/api";
const METRICS: { key: ProgressionMetric; label: string }[] = [
{ key: "pace", label: "Pace" },
{ key: "hr", label: "Heart Rate" },
{ key: "vo2max", label: "VO2max" },
{ key: "aerobic_te", label: "Aerobic Training Effect" },
{ key: "anaerobic_te", label: "Anaerobic Training Effect" },
];
export function Dashboard() {
const [kinds, setKinds] = useState<WorkoutKind[]>([]);
const [selectedKindId, setSelectedKindId] = useState<number | null>(null);
const [metric, setMetric] = useState<ProgressionMetric>("pace");
const [points, setPoints] = useState<ProgressionPoint[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
api
.listWorkoutKinds()
.then((k) => {
setKinds(k);
if (k.length > 0) setSelectedKindId(k[0].ID);
})
.catch((e) => setError(String(e)));
}, []);
useEffect(() => {
if (selectedKindId == null) return;
setLoading(true);
setError(null);
api
.progression(selectedKindId, metric)
.then(setPoints)
.catch((e) => setError(String(e)))
.finally(() => setLoading(false));
}, [selectedKindId, metric]);
return (
<div className="page">
<h2>Progression</h2>
{kinds.length === 0 ? (
<p className="empty-state">
No workout kinds defined yet. Create one on the Workout Kinds tab to start seeing progression here.
</p>
) : (
<>
<div className="controls">
<label>
Workout kind
<select
value={selectedKindId ?? ""}
onChange={(e) => setSelectedKindId(Number(e.target.value))}
>
{kinds.map((k) => (
<option key={k.ID} value={k.ID}>
{k.Name}
</option>
))}
</select>
</label>
<label>
Metric
<select value={metric} onChange={(e) => setMetric(e.target.value as ProgressionMetric)}>
{METRICS.map((m) => (
<option key={m.key} value={m.key}>
{m.label}
</option>
))}
</select>
</label>
</div>
{error && <p className="error">{error}</p>}
{loading ? <p>Loading...</p> : <ProgressionChart points={points} metric={metric} />}
</>
)}
</div>
);
}