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,76 @@
// Package mock provides a fake garmin.Client for tests and frontend/dev
// work without a live Garmin account or the mcp-garmin subprocess.
package mock
import (
"context"
"smartrun/backend/internal/garmin"
)
// Client is a fake garmin.Client returning data supplied by the test/caller.
type Client struct {
AuthResults []garmin.AuthResult // consumed in order by Authenticate/CompleteMFA calls
Activities []garmin.Activity
Splits map[int64]garmin.ActivitySplits
Details map[int64]garmin.ActivityDetails
Err error // if set, every call returns this error
authResultCursor int
ClosedCalled bool
GetActivitiesCalls int
}
var _ garmin.Client = (*Client)(nil)
func (c *Client) nextAuthResult() garmin.AuthResult {
if c.authResultCursor >= len(c.AuthResults) {
return garmin.AuthResult{Status: garmin.AuthSuccess, Message: "Authenticated successfully."}
}
r := c.AuthResults[c.authResultCursor]
c.authResultCursor++
return r
}
func (c *Client) Authenticate(ctx context.Context) (garmin.AuthResult, error) {
if c.Err != nil {
return garmin.AuthResult{}, c.Err
}
return c.nextAuthResult(), nil
}
func (c *Client) CompleteMFA(ctx context.Context, code string) (garmin.AuthResult, error) {
if c.Err != nil {
return garmin.AuthResult{}, c.Err
}
return c.nextAuthResult(), nil
}
func (c *Client) GetActivities(ctx context.Context, startDate, endDate string, limit int) ([]garmin.Activity, error) {
c.GetActivitiesCalls++
if c.Err != nil {
return nil, c.Err
}
if limit > 0 && limit < len(c.Activities) {
return c.Activities[:limit], nil
}
return c.Activities, nil
}
func (c *Client) GetActivitySplits(ctx context.Context, activityID int64) (garmin.ActivitySplits, error) {
if c.Err != nil {
return garmin.ActivitySplits{}, c.Err
}
return c.Splits[activityID], nil
}
func (c *Client) GetActivityDetails(ctx context.Context, activityID int64) (garmin.ActivityDetails, error) {
if c.Err != nil {
return garmin.ActivityDetails{}, c.Err
}
return c.Details[activityID], nil
}
func (c *Client) Close() error {
c.ClosedCalled = true
return nil
}