Rename smartrun to geniusrun throughout the codebase
Updates the Go module path, cmd/smartrund -> cmd/geniusrund, the smartrun-dev skill, .gitignore, and every reference in docs/CLAUDE.md to match.
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
name: smartrun-dev
|
name: geniusrun-dev
|
||||||
description: Use when working on the smartrun repo (backend Go services, classification rule engine, mcp-garmin integration, or frontend) to stay consistent with established conventions.
|
description: Use when working on the geniusrun repo (backend Go services, classification rule engine, mcp-garmin integration, or frontend) to stay consistent with established conventions.
|
||||||
---
|
---
|
||||||
|
|
||||||
# smartrun-dev
|
# geniusrun-dev
|
||||||
|
|
||||||
## Project overview
|
## Project overview
|
||||||
|
|
||||||
smartrun is a personal web app that pulls running activities from Garmin Connect (via the `mcp-garmin` MCP server), classifies each run into a user-defined "workout kind" (Easy, Tempo, Threshold, Interval, ...), and charts progression over time per kind. Ambiguous runs (matching zero, multiple, or only weakly one kind) go to a manual review queue instead of being silently misclassified.
|
geniusrun is a personal web app that pulls running activities from Garmin Connect (via the `mcp-garmin` MCP server), classifies each run into a user-defined "workout kind" (Easy, Tempo, Threshold, Interval, ...), and charts progression over time per kind. Ambiguous runs (matching zero, multiple, or only weakly one kind) go to a manual review queue instead of being silently misclassified.
|
||||||
|
|
||||||
**MVP scope boundary**: sync + classification + review queue + progression charts. A future training-recommendation engine (analyzing aerobic/anaerobic training-effect balance across kinds to suggest what to train next) is explicitly deferred — the schema stores the metrics it would need, but nothing consumes them yet.
|
**MVP scope boundary**: sync + classification + review queue + progression charts. A future training-recommendation engine (analyzing aerobic/anaerobic training-effect balance across kinds to suggest what to train next) is explicitly deferred — the schema stores the metrics it would need, but nothing consumes them yet.
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ smartrun is a personal web app that pulls running activities from Garmin Connect
|
|||||||
|
|
||||||
```
|
```
|
||||||
backend/
|
backend/
|
||||||
cmd/smartrund/ main server entrypoint
|
cmd/geniusrund/ main server entrypoint
|
||||||
cmd/seedsample/ inserts synthetic data for frontend dev/demoing without live Garmin creds
|
cmd/seedsample/ inserts synthetic data for frontend dev/demoing without live Garmin creds
|
||||||
cmd/mcpspike/ throwaway MCP-client spike, safe to delete
|
cmd/mcpspike/ throwaway MCP-client spike, safe to delete
|
||||||
internal/garmin/ MCP client wrapper (auth, get_activities, get_activity_splits, get_activity_details)
|
internal/garmin/ MCP client wrapper (auth, get_activities, get_activity_splits, get_activity_details)
|
||||||
@@ -75,9 +75,9 @@ Each `workout_kinds.rule_json` is a recursive AND/OR condition tree (`internal/c
|
|||||||
|
|
||||||
## Dev workflow
|
## Dev workflow
|
||||||
|
|
||||||
- Backend: `cd backend && go run ./cmd/smartrund` (needs `MCP_GARMIN_PYTHON`/`MCP_GARMIN_SERVER` env vars for the mcp-garmin subprocess paths; see `internal/config/config.go` for all knobs). Garmin credentials are **not** env vars — they live in the `profile` singleton row (`internal/store.Profile`), set via the frontend's Profile page or directly through `PUT /api/profile`.
|
- Backend: `cd backend && go run ./cmd/geniusrund` (needs `MCP_GARMIN_PYTHON`/`MCP_GARMIN_SERVER` env vars for the mcp-garmin subprocess paths; see `internal/config/config.go` for all knobs). Garmin credentials are **not** env vars — they live in the `profile` singleton row (`internal/store.Profile`), set via the frontend's Profile page or directly through `PUT /api/profile`.
|
||||||
- Frontend: `cd frontend && npm run dev` (set `VITE_API_BASE_URL` if the backend isn't on `localhost:8080`).
|
- Frontend: `cd frontend && npm run dev` (set `VITE_API_BASE_URL` if the backend isn't on `localhost:8080`).
|
||||||
- No live Garmin account needed for frontend/UI work: `go run ./cmd/seedsample -db /tmp/sample.db` seeds realistic activities/laps/kinds and runs them through the real classification engine, then point `smartrund` at that DB.
|
- No live Garmin account needed for frontend/UI work: `go run ./cmd/seedsample -db /tmp/sample.db` seeds realistic activities/laps/kinds and runs them through the real classification engine, then point `geniusrund` at that DB.
|
||||||
- `internal/garmin/mock` provides a fake `Client` for tests that need to exercise `internal/sync`/`internal/api` without a live subprocess.
|
- `internal/garmin/mock` provides a fake `Client` for tests that need to exercise `internal/sync`/`internal/api` without a live subprocess.
|
||||||
- Migrations: add a new numbered file under `internal/store/migrations/`, never edit an already-applied one (the runner tracks applied filenames in a `schema_migrations` table).
|
- Migrations: add a new numbered file under `internal/store/migrations/`, never edit an already-applied one (the runner tracks applied filenames in a `schema_migrations` table).
|
||||||
|
|
||||||
@@ -87,4 +87,4 @@ Each `workout_kinds.rule_json` is a recursive AND/OR condition tree (`internal/c
|
|||||||
- `internal/classify` tests are pure (no DB/network): construct a `MetricContext` + `RuleKind`s directly.
|
- `internal/classify` tests are pure (no DB/network): construct a `MetricContext` + `RuleKind`s directly.
|
||||||
- `internal/store` and `internal/sync` tests open a real temp-file SQLite DB (`store.Open` against `t.TempDir()`) — this is deliberate, not mocked, since the migration/SQL correctness is exactly what needs catching.
|
- `internal/store` and `internal/sync` tests open a real temp-file SQLite DB (`store.Open` against `t.TempDir()`) — this is deliberate, not mocked, since the migration/SQL correctness is exactly what needs catching.
|
||||||
- `internal/api` tests use `httptest` against a `Server` wired to a temp DB + `mock.Client`.
|
- `internal/api` tests use `httptest` against a `Server` wired to a temp DB + `mock.Client`.
|
||||||
- The MCP/Garmin integration itself can't be safely automated (real account, MFA, rate limits) — it's a manual smoke test via `cmd/mcpspike` or the real `smartrund` auth endpoints.
|
- The MCP/Garmin integration itself can't be safely automated (real account, MFA, rate limits) — it's a manual smoke test via `cmd/mcpspike` or the real `geniusrund` auth endpoints.
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
# Go
|
# Go
|
||||||
backend/smartrund
|
backend/geniusrund
|
||||||
backend/*.db
|
backend/*.db
|
||||||
backend/*.db-journal
|
backend/*.db-journal
|
||||||
|
|
||||||
|
|||||||
12
CLAUDE.md
12
CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project overview
|
## Project overview
|
||||||
|
|
||||||
geniusrun (Go module `smartrun/backend`) is a personal web app that pulls running activities from Garmin Connect (via the `mcp-garmin` MCP server), classifies each run into one of a fixed set of running-specific "workout kinds" (Easy, Long, 60' Threshold, 30' Threshold, Tempo, Intervals, MAS Test, Race), and charts progression over time per kind. Ambiguous runs (matching zero, multiple, or only weakly one kind) go to a manual review queue instead of being silently misclassified.
|
geniusrun (Go module `geniusrun/backend`) is a personal web app that pulls running activities from Garmin Connect (via the `mcp-garmin` MCP server), classifies each run into one of a fixed set of running-specific "workout kinds" (Easy, Long, 60' Threshold, 30' Threshold, Tempo, Intervals, MAS Test, Race), and charts progression over time per kind. Ambiguous runs (matching zero, multiple, or only weakly one kind) go to a manual review queue instead of being silently misclassified.
|
||||||
|
|
||||||
All Garmin credentials and every tunable analysis-engine parameter (HR zones, phase-detection minutes, pace-artifact filtering, chart colors, etc.) live in a single-row `profile` table, edited from the frontend's Profile screen — there is no multi-profile support, and `internal/config`'s env vars are limited to process-level plumbing (listen addr, DB path, mcp-garmin subprocess paths).
|
All Garmin credentials and every tunable analysis-engine parameter (HR zones, phase-detection minutes, pace-artifact filtering, chart colors, etc.) live in a single-row `profile` table, edited from the frontend's Profile screen — there is no multi-profile support, and `internal/config`'s env vars are limited to process-level plumbing (listen addr, DB path, mcp-garmin subprocess paths).
|
||||||
|
|
||||||
@@ -13,13 +13,13 @@ The "Training plan" tab (`frontend/src/pages/Plan.tsx`) is an intentional empty
|
|||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
Backend (from `backend/`):
|
Backend (from `backend/`):
|
||||||
- Run the server: `./start.sh` (wraps `go run ./cmd/smartrund` with the local mcp-garmin subprocess paths and DB path already set) or `go run ./cmd/smartrund` directly if you export `MCP_GARMIN_PYTHON`/`MCP_GARMIN_SERVER` yourself.
|
- Run the server: `./start.sh` (wraps `go run ./cmd/geniusrund` with the local mcp-garmin subprocess paths and DB path already set) or `go run ./cmd/geniusrund` directly if you export `MCP_GARMIN_PYTHON`/`MCP_GARMIN_SERVER` yourself.
|
||||||
- Build/vet: `go build ./...` && `go vet ./...`
|
- Build/vet: `go build ./...` && `go vet ./...`
|
||||||
- Format: `gofmt -l .` must report nothing before committing.
|
- Format: `gofmt -l .` must report nothing before committing.
|
||||||
- All tests: `go test ./...`
|
- All tests: `go test ./...`
|
||||||
- Single package: `go test ./internal/store/...`
|
- Single package: `go test ./internal/store/...`
|
||||||
- Single test: `go test ./internal/store/... -run TestProfile -v`
|
- Single test: `go test ./internal/store/... -run TestProfile -v`
|
||||||
- Seed sample data (no live Garmin account needed): `go run ./cmd/seedsample -db /tmp/sample.db`, then point `smartrund` at that DB via `SMARTRUN_DB_PATH`.
|
- Seed sample data (no live Garmin account needed): `go run ./cmd/seedsample -db /tmp/sample.db`, then point `geniusrund` at that DB via `GENIUSRUN_DB_PATH`.
|
||||||
- Migrations live in `internal/store/migrations/`; add a new numbered file, never edit an already-applied one (the runner tracks applied filenames in a `schema_migrations` table).
|
- Migrations live in `internal/store/migrations/`; add a new numbered file, never edit an already-applied one (the runner tracks applied filenames in a `schema_migrations` table).
|
||||||
|
|
||||||
Frontend (from `frontend/`):
|
Frontend (from `frontend/`):
|
||||||
@@ -32,7 +32,7 @@ Frontend (from `frontend/`):
|
|||||||
|
|
||||||
```
|
```
|
||||||
backend/
|
backend/
|
||||||
cmd/smartrund/ main server entrypoint
|
cmd/geniusrund/ main server entrypoint
|
||||||
cmd/seedsample/ inserts synthetic data for frontend dev/demoing without live Garmin creds
|
cmd/seedsample/ inserts synthetic data for frontend dev/demoing without live Garmin creds
|
||||||
cmd/mcpspike/ throwaway MCP-client spike, safe to delete
|
cmd/mcpspike/ throwaway MCP-client spike, safe to delete
|
||||||
internal/garmin/ MCP client wrapper (auth, get_activities, get_activity_splits, get_activity_details, get_workout_by_id)
|
internal/garmin/ MCP client wrapper (auth, get_activities, get_activity_splits, get_activity_details, get_workout_by_id)
|
||||||
@@ -68,7 +68,7 @@ Each `workout_kinds.rule_json` is a recursive AND/OR condition tree (`internal/c
|
|||||||
- Leaf conditions: `{metric, op, value}`. `op` is `==`, `!=`, `>`, `>=`, `<`, `<=`, or `between` (value is a 2-element array).
|
- Leaf conditions: `{metric, op, value}`. `op` is `==`, `!=`, `>`, `>=`, `<`, `<=`, or `between` (value is a 2-element array).
|
||||||
- Supported metrics (see `internal/sync/mapping.go`'s `buildMetricContext`): `avg_pace_sec_per_km`, `avg_hr`, `avg_hr_pct_max`, `max_hr`, `duration_seconds`, `distance_meters`, `elevation_gain_m`, `aerobic_training_effect`, `anaerobic_training_effect`, `vo2max_value`, `is_race`, `lap_interval_pattern` (0/1), `lap_pace_stddev`, `lap_hr_drift_bpm_per_min`, `lap_hr_recovery_bpm_per_min`.
|
- Supported metrics (see `internal/sync/mapping.go`'s `buildMetricContext`): `avg_pace_sec_per_km`, `avg_hr`, `avg_hr_pct_max`, `max_hr`, `duration_seconds`, `distance_meters`, `elevation_gain_m`, `aerobic_training_effect`, `anaerobic_training_effect`, `vo2max_value`, `is_race`, `lap_interval_pattern` (0/1), `lap_pace_stddev`, `lap_hr_drift_bpm_per_min`, `lap_hr_recovery_bpm_per_min`.
|
||||||
- **Scoring**: each leaf gets a margin-based confidence in ~[0,1] (`between` = distance from center; comparisons = logistic squash of margin past the threshold). Branches aggregate via `min` (AND) / `max` (OR) — no ML, fully explainable.
|
- **Scoring**: each leaf gets a margin-based confidence in ~[0,1] (`between` = distance from center; comparisons = logistic squash of margin past the threshold). Branches aggregate via `min` (AND) / `max` (OR) — no ML, fully explainable.
|
||||||
- **`needs_review` triggers** (in `classify.Classify`): zero kinds matched, 2+ kinds matched, or exactly one matched below `min_confidence` (default from `SMARTRUN_MIN_CONFIDENCE`, 0.6). All three populate `Candidates` for the review UI.
|
- **`needs_review` triggers** (in `classify.Classify`): zero kinds matched, 2+ kinds matched, or exactly one matched below `min_confidence` (default from `GENIUSRUN_MIN_CONFIDENCE`, 0.6). All three populate `Candidates` for the review UI.
|
||||||
- **Interval detection** (`classify.DetectIntervalPattern`) trusts Garmin's own per-lap `IntensityType` tagging (ACTIVE vs REST/RECOVERY/WARMUP/COOLDOWN) rather than inferring it from pace variance.
|
- **Interval detection** (`classify.DetectIntervalPattern`) trusts Garmin's own per-lap `IntensityType` tagging (ACTIVE vs REST/RECOVERY/WARMUP/COOLDOWN) rather than inferring it from pace variance.
|
||||||
- **HR drift/recovery** (`classify.HRDrift`/`HRRecovery`): linear regression of heart rate vs elapsed time within a lap's sample window. Drift = rising HR during an active lap. Recovery = HR decay rate during a rest lap, sign-flipped so positive = good recovery.
|
- **HR drift/recovery** (`classify.HRDrift`/`HRRecovery`): linear regression of heart rate vs elapsed time within a lap's sample window. Drift = rising HR during an active lap. Recovery = HR decay rate during a rest lap, sign-flipped so positive = good recovery.
|
||||||
- Max HR for `avg_hr_pct_max` comes from `profile.max_heart_rate`, not a static config value — nil/unset omits that metric from the context rather than erroring.
|
- Max HR for `avg_hr_pct_max` comes from `profile.max_heart_rate`, not a static config value — nil/unset omits that metric from the context rather than erroring.
|
||||||
@@ -109,4 +109,4 @@ Each `workout_kinds.rule_json` is a recursive AND/OR condition tree (`internal/c
|
|||||||
- `internal/classify` tests are pure (no DB/network): construct a `MetricContext` + `RuleKind`s directly.
|
- `internal/classify` tests are pure (no DB/network): construct a `MetricContext` + `RuleKind`s directly.
|
||||||
- `internal/store` and `internal/sync` tests open a real temp-file SQLite DB (`store.Open` against `t.TempDir()`) — deliberate, not mocked, since migration/SQL correctness is exactly what needs catching.
|
- `internal/store` and `internal/sync` tests open a real temp-file SQLite DB (`store.Open` against `t.TempDir()`) — deliberate, not mocked, since migration/SQL correctness is exactly what needs catching.
|
||||||
- `internal/api` tests use `httptest` against a `Server` wired to a temp DB + `mock.Client`.
|
- `internal/api` tests use `httptest` against a `Server` wired to a temp DB + `mock.Client`.
|
||||||
- The MCP/Garmin integration itself can't be safely automated (real account, MFA, rate limits) — it's a manual smoke test via `cmd/mcpspike` or the real `smartrund` auth endpoints.
|
- The MCP/Garmin integration itself can't be safely automated (real account, MFA, rate limits) — it's a manual smoke test via `cmd/mcpspike` or the real `geniusrund` auth endpoints.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Command smartrund is smartrun's backend server: syncs runs from Garmin,
|
// Command geniusrund is geniusrun's backend server: syncs runs from Garmin,
|
||||||
// classifies them into workout kinds, and serves the REST API the frontend
|
// classifies them into workout kinds, and serves the REST API the frontend
|
||||||
// talks to.
|
// talks to.
|
||||||
package main
|
package main
|
||||||
@@ -11,11 +11,11 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"smartrun/backend/internal/api"
|
"geniusrun/backend/internal/api"
|
||||||
"smartrun/backend/internal/config"
|
"geniusrun/backend/internal/config"
|
||||||
"smartrun/backend/internal/garmin"
|
"geniusrun/backend/internal/garmin"
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
appsync "smartrun/backend/internal/sync"
|
appsync "geniusrun/backend/internal/sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -57,7 +57,7 @@ func main() {
|
|||||||
|
|
||||||
httpServer := &http.Server{Addr: cfg.Addr, Handler: server.Router()}
|
httpServer := &http.Server{Addr: cfg.Addr, Handler: server.Router()}
|
||||||
go func() {
|
go func() {
|
||||||
log.Printf("smartrund listening on %s", cfg.Addr)
|
log.Printf("geniusrund listening on %s", cfg.Addr)
|
||||||
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
log.Fatalf("http server: %v", err)
|
log.Fatalf("http server: %v", err)
|
||||||
}
|
}
|
||||||
@@ -12,14 +12,14 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"smartrun/backend/internal/classify"
|
"geniusrun/backend/internal/classify"
|
||||||
"smartrun/backend/internal/garmin/mock"
|
"geniusrun/backend/internal/garmin/mock"
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
appsync "smartrun/backend/internal/sync"
|
appsync "geniusrun/backend/internal/sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
dbPath := flag.String("db", "smartrun_sample.db", "path to the SQLite database to seed")
|
dbPath := flag.String("db", "geniusrun_sample.db", "path to the SQLite database to seed")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
module smartrun/backend
|
module geniusrun/backend
|
||||||
|
|
||||||
go 1.26.4
|
go 1.26.4
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// activityListItem is one row of the activities list, enriched with its
|
// activityListItem is one row of the activities list, enriched with its
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"smartrun/backend/internal/garmin/mock"
|
"geniusrun/backend/internal/garmin/mock"
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
appsync "smartrun/backend/internal/sync"
|
appsync "geniusrun/backend/internal/sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newCtx() context.Context { return context.Background() }
|
func newCtx() context.Context { return context.Background() }
|
||||||
|
|
||||||
func newTestServer(t *testing.T) (*Server, *store.DB) {
|
func newTestServer(t *testing.T) (*Server, *store.DB) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
db, err := store.Open(filepath.Join(t.TempDir(), "smartrun_test.db"))
|
db, err := store.Open(filepath.Join(t.TempDir(), "geniusrun_test.db"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("store.Open: %v", err)
|
t.Fatalf("store.Open: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"smartrun/backend/internal/garmin"
|
"geniusrun/backend/internal/garmin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type authResponse struct {
|
type authResponse struct {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// activityResponse adds back ActivityName/ActivityType as fields decoded
|
// activityResponse adds back ActivityName/ActivityType as fields decoded
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"smartrun/backend/internal/classify"
|
"geniusrun/backend/internal/classify"
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// workoutKindResponse combines a workout kind's rule/metadata with its pace
|
// workoutKindResponse combines a workout kind's rule/metadata with its pace
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// validateProfile checks the HR zones are ascending and gap-free once they
|
// validateProfile checks the HR zones are ascending and gap-free once they
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
type progressionPoint struct {
|
type progressionPoint struct {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleReclassifyAll re-runs the rule engine for every activity except
|
// handleReclassifyAll re-runs the rule engine for every activity except
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultReviewQueuePageSize matches the frontend's initial/incremental
|
// defaultReviewQueuePageSize matches the frontend's initial/incremental
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Package api is smartrun's HTTP layer: REST handlers over internal/store,
|
// Package api is geniusrun's HTTP layer: REST handlers over internal/store,
|
||||||
// internal/garmin, and internal/sync.
|
// internal/garmin, and internal/sync.
|
||||||
package api
|
package api
|
||||||
|
|
||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"smartrun/backend/internal/garmin"
|
"geniusrun/backend/internal/garmin"
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
appsync "smartrun/backend/internal/sync"
|
appsync "geniusrun/backend/internal/sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server wires the HTTP handlers to the app's dependencies.
|
// Server wires the HTTP handlers to the app's dependencies.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Package classify is smartrun's classification rule engine: it evaluates a
|
// Package classify is geniusrun's classification rule engine: it evaluates a
|
||||||
// user-editable AND/OR condition tree against a run's metrics to decide
|
// user-editable AND/OR condition tree against a run's metrics to decide
|
||||||
// which "workout kind" (Easy, Tempo, Threshold, ...) it belongs to. Pure
|
// which "workout kind" (Easy, Tempo, Threshold, ...) it belongs to. Pure
|
||||||
// logic only -- no I/O, no database, no Garmin client -- so it's fully
|
// logic only -- no I/O, no database, no Garmin client -- so it's fully
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Package config loads smartrund's runtime infrastructure configuration
|
// Package config loads geniusrund's runtime infrastructure configuration
|
||||||
// from environment variables (paths and process settings that don't belong
|
// from environment variables (paths and process settings that don't belong
|
||||||
// in the user-editable profile). Garmin credentials and every tunable
|
// in the user-editable profile). Garmin credentials and every tunable
|
||||||
// analysis-engine parameter live in the profile (internal/store.Profile)
|
// analysis-engine parameter live in the profile (internal/store.Profile)
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config holds smartrund's process-level configuration.
|
// Config holds geniusrund's process-level configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Addr is the HTTP listen address, e.g. ":8080".
|
// Addr is the HTTP listen address, e.g. ":8080".
|
||||||
Addr string
|
Addr string
|
||||||
@@ -35,13 +35,13 @@ type Config struct {
|
|||||||
// for anything optional. Returns an error if a required variable is unset.
|
// for anything optional. Returns an error if a required variable is unset.
|
||||||
func Load() (Config, error) {
|
func Load() (Config, error) {
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
Addr: getEnvDefault("SMARTRUN_ADDR", ":8080"),
|
Addr: getEnvDefault("GENIUSRUN_ADDR", ":8080"),
|
||||||
DBPath: getEnvDefault("SMARTRUN_DB_PATH", "smartrun.db"),
|
DBPath: getEnvDefault("GENIUSRUN_DB_PATH", "geniusrun.db"),
|
||||||
GarminPythonPath: os.Getenv("MCP_GARMIN_PYTHON"),
|
GarminPythonPath: os.Getenv("MCP_GARMIN_PYTHON"),
|
||||||
GarminServerPath: os.Getenv("MCP_GARMIN_SERVER"),
|
GarminServerPath: os.Getenv("MCP_GARMIN_SERVER"),
|
||||||
GarminTokenStore: os.Getenv("GARMIN_TOKENSTORE"),
|
GarminTokenStore: os.Getenv("GARMIN_TOKENSTORE"),
|
||||||
MinConfidence: getEnvFloat("SMARTRUN_MIN_CONFIDENCE", 0.6),
|
MinConfidence: getEnvFloat("GENIUSRUN_MIN_CONFIDENCE", 0.6),
|
||||||
IncrementalSyncEvery: getEnvDuration("SMARTRUN_INCREMENTAL_SYNC_EVERY", 6*time.Hour),
|
IncrementalSyncEvery: getEnvDuration("GENIUSRUN_INCREMENTAL_SYNC_EVERY", 6*time.Hour),
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.GarminPythonPath == "" {
|
if cfg.GarminPythonPath == "" {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Package garmin wraps the mcp-garmin MCP server as a narrow Go client
|
// Package garmin wraps the mcp-garmin MCP server as a narrow Go client
|
||||||
// interface, so the rest of smartrun never deals with MCP/JSON-RPC directly.
|
// interface, so the rest of geniusrun never deals with MCP/JSON-RPC directly.
|
||||||
package garmin
|
package garmin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client is the interface the rest of smartrun depends on. The real
|
// Client is the interface the rest of geniusrun depends on. The real
|
||||||
// implementation drives mcp-garmin over stdio; internal/garmin/mock provides
|
// implementation drives mcp-garmin over stdio; internal/garmin/mock provides
|
||||||
// a fake for tests and frontend-only development.
|
// a fake for tests and frontend-only development.
|
||||||
type Client interface {
|
type Client interface {
|
||||||
@@ -94,7 +94,7 @@ func (c *mcpClient) ensureStarted(ctx context.Context) error {
|
|||||||
|
|
||||||
initReq := mcp.InitializeRequest{}
|
initReq := mcp.InitializeRequest{}
|
||||||
initReq.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
|
initReq.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
|
||||||
initReq.Params.ClientInfo = mcp.Implementation{Name: "smartrund", Version: "0.0.1"}
|
initReq.Params.ClientInfo = mcp.Implementation{Name: "geniusrund", Version: "0.0.1"}
|
||||||
if _, err := inner.Initialize(ctx, initReq); err != nil {
|
if _, err := inner.Initialize(ctx, initReq); err != nil {
|
||||||
inner.Close()
|
inner.Close()
|
||||||
return fmt.Errorf("mcp initialize handshake: %w", err)
|
return fmt.Errorf("mcp initialize handshake: %w", err)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ package mock
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"smartrun/backend/internal/garmin"
|
"geniusrun/backend/internal/garmin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client is a fake garmin.Client returning data supplied by the test/caller.
|
// Client is a fake garmin.Client returning data supplied by the test/caller.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Activity is smartrun's persisted view of a Garmin activity. Field mapping
|
// Activity is geniusrun's persisted view of a Garmin activity. Field mapping
|
||||||
// from the Garmin/MCP response happens in internal/sync, not here, so this
|
// from the Garmin/MCP response happens in internal/sync, not here, so this
|
||||||
// package stays independent of internal/garmin.
|
// package stays independent of internal/garmin.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Package store is smartrun's SQLite persistence layer: activities, laps,
|
// Package store is geniusrun's SQLite persistence layer: activities, laps,
|
||||||
// per-second samples, workout kind rule config, and classification history.
|
// per-second samples, workout kind rule config, and classification history.
|
||||||
package store
|
package store
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*.sql
|
||||||
var migrationsFS embed.FS
|
var migrationsFS embed.FS
|
||||||
|
|
||||||
// DB wraps a *sql.DB opened against a smartrun SQLite database file, with
|
// DB wraps a *sql.DB opened against a geniusrun SQLite database file, with
|
||||||
// migrations already applied.
|
// migrations already applied.
|
||||||
type DB struct {
|
type DB struct {
|
||||||
*sql.DB
|
*sql.DB
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
-- Backfill horizon used to be a startup-time env var
|
-- Backfill horizon used to be a startup-time env var
|
||||||
-- (SMARTRUN_BACKFILL_HORIZON_DAYS) with no UI at all -- exactly the kind of
|
-- (GENIUSRUN_BACKFILL_HORIZON_DAYS) with no UI at all -- exactly the kind of
|
||||||
-- "tunable analysis-engine parameter" this profile table exists for.
|
-- "tunable analysis-engine parameter" this profile table exists for.
|
||||||
-- Default matches the old env var's default (3 years) so existing
|
-- Default matches the old env var's default (3 years) so existing
|
||||||
-- deployments keep their current behavior until the user changes it.
|
-- deployments keep their current behavior until the user changes it.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
func openTestDB(t *testing.T) *DB {
|
func openTestDB(t *testing.T) *DB {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
path := filepath.Join(t.TempDir(), "smartrun_test.db")
|
path := filepath.Join(t.TempDir(), "geniusrun_test.db")
|
||||||
db, err := Open(path)
|
db, err := Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Open: %v", err)
|
t.Fatalf("Open: %v", err)
|
||||||
@@ -20,7 +20,7 @@ func openTestDB(t *testing.T) *DB {
|
|||||||
func f(v float64) *float64 { return &v }
|
func f(v float64) *float64 { return &v }
|
||||||
|
|
||||||
func TestMigrateIsIdempotent(t *testing.T) {
|
func TestMigrateIsIdempotent(t *testing.T) {
|
||||||
path := filepath.Join(t.TempDir(), "smartrun_test.db")
|
path := filepath.Join(t.TempDir(), "geniusrun_test.db")
|
||||||
db1, err := Open(path)
|
db1, err := Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("first Open: %v", err)
|
t.Fatalf("first Open: %v", err)
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"smartrun/backend/internal/classify"
|
"geniusrun/backend/internal/classify"
|
||||||
"smartrun/backend/internal/garmin"
|
"geniusrun/backend/internal/garmin"
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// isRunningActivityType reports whether a Garmin activityType.typeKey
|
// isRunningActivityType reports whether a Garmin activityType.typeKey
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"smartrun/backend/internal/classify"
|
"geniusrun/backend/internal/classify"
|
||||||
"smartrun/backend/internal/garmin"
|
"geniusrun/backend/internal/garmin"
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config tunes sync behavior. Zero values fall back to sensible defaults in
|
// Config tunes sync behavior. Zero values fall back to sensible defaults in
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"smartrun/backend/internal/classify"
|
"geniusrun/backend/internal/classify"
|
||||||
"smartrun/backend/internal/garmin"
|
"geniusrun/backend/internal/garmin"
|
||||||
"smartrun/backend/internal/garmin/mock"
|
"geniusrun/backend/internal/garmin/mock"
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func f(v float64) *float64 { return &v }
|
func f(v float64) *float64 { return &v }
|
||||||
|
|
||||||
func openTestDB(t *testing.T) *store.DB {
|
func openTestDB(t *testing.T) *store.DB {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
db, err := store.Open(filepath.Join(t.TempDir(), "smartrun_test.db"))
|
db, err := store.Open(filepath.Join(t.TempDir(), "geniusrun_test.db"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("store.Open: %v", err)
|
t.Fatalf("store.Open: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ cd "$(dirname "$0")"
|
|||||||
|
|
||||||
export MCP_GARMIN_PYTHON="${MCP_GARMIN_PYTHON:-/Users/cvila/Dev/scm/scm.vilanet.fr/kriss/mcp-garmin/.venv/bin/python3}"
|
export MCP_GARMIN_PYTHON="${MCP_GARMIN_PYTHON:-/Users/cvila/Dev/scm/scm.vilanet.fr/kriss/mcp-garmin/.venv/bin/python3}"
|
||||||
export MCP_GARMIN_SERVER="${MCP_GARMIN_SERVER:-/Users/cvila/Dev/scm/scm.vilanet.fr/kriss/mcp-garmin/server.py}"
|
export MCP_GARMIN_SERVER="${MCP_GARMIN_SERVER:-/Users/cvila/Dev/scm/scm.vilanet.fr/kriss/mcp-garmin/server.py}"
|
||||||
export SMARTRUN_DB_PATH="${SMARTRUN_DB_PATH:-smartrun.db}"
|
export GENIUSRUN_DB_PATH="${GENIUSRUN_DB_PATH:-geniusrun.db}"
|
||||||
|
|
||||||
exec go run ./cmd/smartrund
|
exec go run ./cmd/geniusrund
|
||||||
|
|||||||
@@ -774,7 +774,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func validateProfile(p store.Profile) error {
|
func validateProfile(p store.Profile) error {
|
||||||
@@ -1007,8 +1007,8 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"smartrun/backend/internal/classify"
|
"geniusrun/backend/internal/classify"
|
||||||
"smartrun/backend/internal/store"
|
"geniusrun/backend/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// workoutKindResponse combines a workout kind's rule/metadata with its pace
|
// workoutKindResponse combines a workout kind's rule/metadata with its pace
|
||||||
@@ -1376,7 +1376,7 @@ Expected: all PASS. `TestBuildMetricContext_DerivesExpectedMetrics` is untouched
|
|||||||
- [ ] **Step 5: Run the full backend test suite**
|
- [ ] **Step 5: Run the full backend test suite**
|
||||||
|
|
||||||
Run: `cd backend && go build ./... && go vet ./... && go test ./...`
|
Run: `cd backend && go build ./... && go vet ./... && go test ./...`
|
||||||
Expected: all PASS. If `cmd/smartrund/main.go` fails to build because it still references `MaxHR: cfg.MaxHR` in its `appsync.Config{...}` literal, that's expected — Task 8 fixes `cmd/smartrund` and `internal/config` together; this step is allowed to show that build failure so you can confirm it's exactly (and only) that one line before moving to Task 8.
|
Expected: all PASS. If `cmd/geniusrund/main.go` fails to build because it still references `MaxHR: cfg.MaxHR` in its `appsync.Config{...}` literal, that's expected — Task 8 fixes `cmd/geniusrund` and `internal/config` together; this step is allowed to show that build failure so you can confirm it's exactly (and only) that one line before moving to Task 8.
|
||||||
|
|
||||||
- [ ] **Step 6: Commit**
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
@@ -1387,22 +1387,22 @@ git commit -m "feat: classification reads max HR from the profile instead of sta
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Task 8: `internal/config` and `cmd/smartrund` — credentials come from the profile
|
### Task 8: `internal/config` and `cmd/geniusrund` — credentials come from the profile
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `backend/internal/config/config.go` (full rewrite — small file, shown complete below)
|
- Modify: `backend/internal/config/config.go` (full rewrite — small file, shown complete below)
|
||||||
- Modify: `backend/cmd/smartrund/main.go` (lines 21-46)
|
- Modify: `backend/cmd/geniusrund/main.go` (lines 21-46)
|
||||||
|
|
||||||
**Interfaces:**
|
**Interfaces:**
|
||||||
- Consumes: `db.GetProfile` (Task 1).
|
- Consumes: `db.GetProfile` (Task 1).
|
||||||
- Removes: `config.Config.GarminEmail`, `config.Config.GarminPassword`, `config.Config.MaxHR` and the `GARMIN_EMAIL`/`GARMIN_PASSWORD`/`SMARTRUN_MAX_HR` env vars — `MCP_GARMIN_PYTHON`/`MCP_GARMIN_SERVER` remain required env vars (infra paths, not secrets).
|
- Removes: `config.Config.GarminEmail`, `config.Config.GarminPassword`, `config.Config.MaxHR` and the `GARMIN_EMAIL`/`GARMIN_PASSWORD`/`GENIUSRUN_MAX_HR` env vars — `MCP_GARMIN_PYTHON`/`MCP_GARMIN_SERVER` remain required env vars (infra paths, not secrets).
|
||||||
|
|
||||||
- [ ] **Step 1: Rewrite `internal/config/config.go`**
|
- [ ] **Step 1: Rewrite `internal/config/config.go`**
|
||||||
|
|
||||||
Replace the full contents of `backend/internal/config/config.go` with:
|
Replace the full contents of `backend/internal/config/config.go` with:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Package config loads smartrund's runtime infrastructure configuration
|
// Package config loads geniusrund's runtime infrastructure configuration
|
||||||
// from environment variables (paths and process settings that don't belong
|
// from environment variables (paths and process settings that don't belong
|
||||||
// in the user-editable profile). Garmin credentials and every tunable
|
// in the user-editable profile). Garmin credentials and every tunable
|
||||||
// analysis-engine parameter live in the profile (internal/store.Profile)
|
// analysis-engine parameter live in the profile (internal/store.Profile)
|
||||||
@@ -1416,7 +1416,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config holds smartrund's process-level configuration.
|
// Config holds geniusrund's process-level configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Addr is the HTTP listen address, e.g. ":8080".
|
// Addr is the HTTP listen address, e.g. ":8080".
|
||||||
Addr string
|
Addr string
|
||||||
@@ -1440,14 +1440,14 @@ type Config struct {
|
|||||||
// for anything optional. Returns an error if a required variable is unset.
|
// for anything optional. Returns an error if a required variable is unset.
|
||||||
func Load() (Config, error) {
|
func Load() (Config, error) {
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
Addr: getEnvDefault("SMARTRUN_ADDR", ":8080"),
|
Addr: getEnvDefault("GENIUSRUN_ADDR", ":8080"),
|
||||||
DBPath: getEnvDefault("SMARTRUN_DB_PATH", "smartrun.db"),
|
DBPath: getEnvDefault("GENIUSRUN_DB_PATH", "geniusrun.db"),
|
||||||
GarminPythonPath: os.Getenv("MCP_GARMIN_PYTHON"),
|
GarminPythonPath: os.Getenv("MCP_GARMIN_PYTHON"),
|
||||||
GarminServerPath: os.Getenv("MCP_GARMIN_SERVER"),
|
GarminServerPath: os.Getenv("MCP_GARMIN_SERVER"),
|
||||||
GarminTokenStore: os.Getenv("GARMIN_TOKENSTORE"),
|
GarminTokenStore: os.Getenv("GARMIN_TOKENSTORE"),
|
||||||
MinConfidence: getEnvFloat("SMARTRUN_MIN_CONFIDENCE", 0.6),
|
MinConfidence: getEnvFloat("GENIUSRUN_MIN_CONFIDENCE", 0.6),
|
||||||
BackfillHorizonDays: getEnvInt("SMARTRUN_BACKFILL_HORIZON_DAYS", 3*365),
|
BackfillHorizonDays: getEnvInt("GENIUSRUN_BACKFILL_HORIZON_DAYS", 3*365),
|
||||||
IncrementalSyncEvery: getEnvDuration("SMARTRUN_INCREMENTAL_SYNC_EVERY", 6*time.Hour),
|
IncrementalSyncEvery: getEnvDuration("GENIUSRUN_INCREMENTAL_SYNC_EVERY", 6*time.Hour),
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.GarminPythonPath == "" {
|
if cfg.GarminPythonPath == "" {
|
||||||
@@ -1494,9 +1494,9 @@ func getEnvDuration(key string, def time.Duration) time.Duration {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Update `cmd/smartrund/main.go` to load credentials from the profile**
|
- [ ] **Step 2: Update `cmd/geniusrund/main.go` to load credentials from the profile**
|
||||||
|
|
||||||
`backend/cmd/smartrund/main.go` currently reads (lines 21-46):
|
`backend/cmd/geniusrund/main.go` currently reads (lines 21-46):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func main() {
|
func main() {
|
||||||
@@ -1566,19 +1566,19 @@ func main() {
|
|||||||
|
|
||||||
- [ ] **Step 3: Build and run the full backend test suite**
|
- [ ] **Step 3: Build and run the full backend test suite**
|
||||||
|
|
||||||
Run: `cd backend && gofmt -w internal/config/config.go cmd/smartrund/main.go && go build ./... && go vet ./... && go test ./...`
|
Run: `cd backend && gofmt -w internal/config/config.go cmd/geniusrund/main.go && go build ./... && go vet ./... && go test ./...`
|
||||||
Expected: everything builds and all tests PASS.
|
Expected: everything builds and all tests PASS.
|
||||||
|
|
||||||
- [ ] **Step 4: Manual smoke test — start the server without Garmin env vars**
|
- [ ] **Step 4: Manual smoke test — start the server without Garmin env vars**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
rm -f /tmp/smartrun_task8.db
|
rm -f /tmp/geniusrun_task8.db
|
||||||
MCP_GARMIN_PYTHON=/Users/kriss/Dev/scm/scm.vilanet.fr/kriss/mcp-garmin/.venv/bin/python \
|
MCP_GARMIN_PYTHON=/Users/kriss/Dev/scm/scm.vilanet.fr/kriss/mcp-garmin/.venv/bin/python \
|
||||||
MCP_GARMIN_SERVER=/Users/kriss/Dev/scm/scm.vilanet.fr/kriss/mcp-garmin/server.py \
|
MCP_GARMIN_SERVER=/Users/kriss/Dev/scm/scm.vilanet.fr/kriss/mcp-garmin/server.py \
|
||||||
SMARTRUN_DB_PATH=/tmp/smartrun_task8.db \
|
GENIUSRUN_DB_PATH=/tmp/geniusrun_task8.db \
|
||||||
SMARTRUN_ADDR=:8085 \
|
GENIUSRUN_ADDR=:8085 \
|
||||||
go run ./cmd/smartrund &
|
go run ./cmd/geniusrund &
|
||||||
sleep 2
|
sleep 2
|
||||||
curl -s localhost:8085/api/profile
|
curl -s localhost:8085/api/profile
|
||||||
curl -s localhost:8085/api/workout-kinds/ | head -c 300
|
curl -s localhost:8085/api/workout-kinds/ | head -c 300
|
||||||
@@ -1590,7 +1590,7 @@ Expected: the server starts (no "GARMIN_EMAIL required" error), `/api/profile` r
|
|||||||
- [ ] **Step 5: Commit**
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend && git add internal/config/config.go cmd/smartrund/main.go
|
cd backend && git add internal/config/config.go cmd/geniusrund/main.go
|
||||||
git commit -m "feat: source Garmin credentials from the profile instead of env vars"
|
git commit -m "feat: source Garmin credentials from the profile instead of env vars"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -2307,11 +2307,11 @@ Change to:
|
|||||||
cd backend
|
cd backend
|
||||||
gofmt -w cmd/seedsample/main.go
|
gofmt -w cmd/seedsample/main.go
|
||||||
go build ./... && go vet ./...
|
go build ./... && go vet ./...
|
||||||
rm -f /tmp/smartrun_verify.db
|
rm -f /tmp/geniusrun_verify.db
|
||||||
go run ./cmd/seedsample -db /tmp/smartrun_verify.db
|
go run ./cmd/seedsample -db /tmp/geniusrun_verify.db
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: builds clean, prints `Seeded 10 activities (kinds: Easy=<id>, Tempo=<id>) into /tmp/smartrun_verify.db` with no errors (no unique-constraint failure).
|
Expected: builds clean, prints `Seeded 10 activities (kinds: Easy=<id>, Tempo=<id>) into /tmp/geniusrun_verify.db` with no errors (no unique-constraint failure).
|
||||||
|
|
||||||
- [ ] **Step 5: Run the full backend test suite one more time**
|
- [ ] **Step 5: Run the full backend test suite one more time**
|
||||||
|
|
||||||
@@ -2322,12 +2322,12 @@ Expected: `gofmt -l .` prints nothing; everything else PASSes.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
go build -o /tmp/smartrund ./cmd/smartrund
|
go build -o /tmp/geniusrund ./cmd/geniusrund
|
||||||
MCP_GARMIN_PYTHON=/Users/kriss/Dev/scm/scm.vilanet.fr/kriss/mcp-garmin/.venv/bin/python \
|
MCP_GARMIN_PYTHON=/Users/kriss/Dev/scm/scm.vilanet.fr/kriss/mcp-garmin/.venv/bin/python \
|
||||||
MCP_GARMIN_SERVER=/Users/kriss/Dev/scm/scm.vilanet.fr/kriss/mcp-garmin/server.py \
|
MCP_GARMIN_SERVER=/Users/kriss/Dev/scm/scm.vilanet.fr/kriss/mcp-garmin/server.py \
|
||||||
SMARTRUN_DB_PATH=/tmp/smartrun_verify.db \
|
GENIUSRUN_DB_PATH=/tmp/geniusrun_verify.db \
|
||||||
SMARTRUN_ADDR=:8080 \
|
GENIUSRUN_ADDR=:8080 \
|
||||||
/tmp/smartrund &
|
/tmp/geniusrund &
|
||||||
sleep 1
|
sleep 1
|
||||||
curl -s localhost:8080/api/workout-kinds/ | python3 -m json.tool | head -30
|
curl -s localhost:8080/api/workout-kinds/ | python3 -m json.tool | head -30
|
||||||
curl -s localhost:8080/api/profile | python3 -m json.tool
|
curl -s localhost:8080/api/profile | python3 -m json.tool
|
||||||
@@ -2352,7 +2352,7 @@ Open the printed URL and check:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
kill %1 2>/dev/null
|
kill %1 2>/dev/null
|
||||||
rm -f /tmp/smartrund /tmp/smartrun_verify.db
|
rm -f /tmp/geniusrund /tmp/geniusrun_verify.db
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 9: Commit**
|
- [ ] **Step 9: Commit**
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
smartrun currently classifies runs into arbitrary, user-created "workout
|
geniusrun currently classifies runs into arbitrary, user-created "workout
|
||||||
kinds" using absolute pace/HR thresholds, and has no concept of a user
|
kinds" using absolute pace/HR thresholds, and has no concept of a user
|
||||||
profile — Garmin credentials are passed as environment variables at process
|
profile — Garmin credentials are passed as environment variables at process
|
||||||
start, and there is exactly one hardcoded set of engine parameters.
|
start, and there is exactly one hardcoded set of engine parameters.
|
||||||
@@ -81,8 +81,8 @@ consolidating all tunable engine parameters:
|
|||||||
`easy_warmup_minutes`, `easy_cooldown_minutes`, `interval_warmup_minutes`,
|
`easy_warmup_minutes`, `easy_cooldown_minutes`, `interval_warmup_minutes`,
|
||||||
one pair per non-lap-based type.
|
one pair per non-lap-based type.
|
||||||
|
|
||||||
`internal/config` (env-var loading) shrinks to just `SMARTRUN_ADDR`,
|
`internal/config` (env-var loading) shrinks to just `GENIUSRUN_ADDR`,
|
||||||
`SMARTRUN_DB_PATH`, and the mcp-garmin subprocess paths — everything
|
`GENIUSRUN_DB_PATH`, and the mcp-garmin subprocess paths — everything
|
||||||
runtime-tunable moves into `profile`.
|
runtime-tunable moves into `profile`.
|
||||||
|
|
||||||
**Validation** (rejected at save time, not silently accepted):
|
**Validation** (rejected at save time, not silently accepted):
|
||||||
|
|||||||
Reference in New Issue
Block a user