fix(config): stop deriving GarminTokenStoreRoot's default from DBPath
There's no reason the Garmin token-store root and the SQLite database
file need to live near each other -- they're independent env vars
(GARMIN_TOKENSTORE, GENIUSRUN_DB_PATH) and should stay independently
configurable, including in their defaults. Revert the "next to DBPath"
default introduced in 77f9588 back to a plain ".garmin" relative to
the working directory, so pointing GENIUSRUN_DB_PATH elsewhere never
silently drags the token-store default along with it.
This commit is contained in:
@@ -91,7 +91,7 @@ Each `workout_kinds.rule_json` is a recursive AND/OR condition tree (`internal/c
|
|||||||
|
|
||||||
- **`authenticate`/`complete_mfa` return structured JSON** (`{"status": "success"|"mfa_required"|"failed", "message": "..."}`), not plain strings — `internal/garmin/client.go` maps `status` directly to `AuthStatus`, no string pattern-matching.
|
- **`authenticate`/`complete_mfa` return structured JSON** (`{"status": "success"|"mfa_required"|"failed", "message": "..."}`), not plain strings — `internal/garmin/client.go` maps `status` directly to `AuthStatus`, no string pattern-matching.
|
||||||
- **The 10s "MFA required" timeout in `wrapper.py`'s `authenticate` handler is a false-positive trap.** A login that's merely slow (e.g. Garmin/Cloudflare rate-limiting) looks identical to a real MFA challenge unless you check whether `prompt_mfa()` was actually invoked (`wrapper.py` logs this to stderr for exactly this reason).
|
- **The 10s "MFA required" timeout in `wrapper.py`'s `authenticate` handler is a false-positive trap.** A login that's merely slow (e.g. Garmin/Cloudflare rate-limiting) looks identical to a real MFA challenge unless you check whether `prompt_mfa()` was actually invoked (`wrapper.py` logs this to stderr for exactly this reason).
|
||||||
- **`wrapper.py` persists Garmin sessions** via a `GARMIN_TOKENSTORE` env var passed to `Garmin.login(tokenstore=...)` — without this, every process start does a full SSO login, which is what trips Garmin's rate limiting under repeated testing. Since geniusrun is multi-tenant, `GARMIN_TOKENSTORE` (`config.GarminTokenStoreRoot`) is a **root directory**, not a single session path: `api.Server.garminFor` namespaces each user's subprocess under `{root}/{userID}` so concurrent users never share a Garmin session. If unset, `config.Load()` defaults it to a `.garmin` directory next to the DB file (logged at startup) rather than leaving per-user namespacing silently skipped.
|
- **`wrapper.py` persists Garmin sessions** via a `GARMIN_TOKENSTORE` env var passed to `Garmin.login(tokenstore=...)` — without this, every process start does a full SSO login, which is what trips Garmin's rate limiting under repeated testing. Since geniusrun is multi-tenant, `GARMIN_TOKENSTORE` (`config.GarminTokenStoreRoot`) is a **root directory**, not a single session path: `api.Server.garminFor` namespaces each user's subprocess under `{root}/{userID}` so concurrent users never share a Garmin session. If unset, `config.Load()` defaults it to a `.garmin` directory relative to the working directory -- deliberately independent of `GENIUSRUN_DB_PATH`, so the DB file and the token-store root can each be pointed at their own directory -- rather than leaving per-user namespacing silently skipped.
|
||||||
- **`activityId` is a large int64** — never round-trip it through `float64`/generic `map[string]any` JSON decoding, or it corrupts into scientific notation. Decode into typed structs (`garmin.Activity`, not `map[string]any`).
|
- **`activityId` is a large int64** — never round-trip it through `float64`/generic `map[string]any` JSON decoding, or it corrupts into scientific notation. Decode into typed structs (`garmin.Activity`, not `map[string]any`).
|
||||||
- **`get_activity_details` returns raw per-second telemetry** (`activityDetailMetrics` + `metricDescriptors`), not lap/split summaries. Its `metricDescriptors` index-to-field mapping is **not stable across activities/devices** — `garmin.ExtractSamples` always resolves fields by descriptor key, never by fixed array position.
|
- **`get_activity_details` returns raw per-second telemetry** (`activityDetailMetrics` + `metricDescriptors`), not lap/split summaries. Its `metricDescriptors` index-to-field mapping is **not stable across activities/devices** — `garmin.ExtractSamples` always resolves fields by descriptor key, never by fixed array position.
|
||||||
- **`get_activity_splits`** returns the actual lap/split summaries (`lapDTOs`).
|
- **`get_activity_splits`** returns the actual lap/split summaries (`lapDTOs`).
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ package config
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -27,11 +26,13 @@ type Config struct {
|
|||||||
GarminPythonPath string
|
GarminPythonPath string
|
||||||
// GarminTokenStoreRoot is the root directory under which each user's
|
// GarminTokenStoreRoot is the root directory under which each user's
|
||||||
// Garmin session cache lives (one subdirectory per user id, e.g.
|
// Garmin session cache lives (one subdirectory per user id, e.g.
|
||||||
// "<root>/3"). Read from the GARMIN_TOKENSTORE env var; if unset, it
|
// "<root>/3"). Read from the GARMIN_TOKENSTORE env var, configured
|
||||||
// defaults to a ".garmin" directory next to DBPath so every
|
// independently of DBPath -- if unset, it defaults to a ".garmin"
|
||||||
// deployment gets per-user isolation automatically -- multi-tenant
|
// directory relative to the working directory the process is started
|
||||||
// operation always relies on this being a real, distinct-per-user path
|
// from, not derived from DBPath in any way, so every deployment gets
|
||||||
// (see api.Server.garminFor), so it can never be silently left empty.
|
// per-user isolation automatically -- multi-tenant operation always
|
||||||
|
// relies on this being a real, distinct-per-user path (see
|
||||||
|
// api.Server.garminFor), so it can never be silently left empty.
|
||||||
GarminTokenStoreRoot string
|
GarminTokenStoreRoot string
|
||||||
|
|
||||||
MinConfidence float64
|
MinConfidence float64
|
||||||
@@ -68,12 +69,11 @@ type Config struct {
|
|||||||
// Load reads configuration from environment variables, applying defaults
|
// Load reads configuration from environment variables, applying defaults
|
||||||
// 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) {
|
||||||
dbPath := getEnvDefault("GENIUSRUN_DB_PATH", "geniusrun.db")
|
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
Addr: getEnvDefault("GENIUSRUN_ADDR", ":8080"),
|
Addr: getEnvDefault("GENIUSRUN_ADDR", ":8080"),
|
||||||
DBPath: dbPath,
|
DBPath: getEnvDefault("GENIUSRUN_DB_PATH", "geniusrun.db"),
|
||||||
GarminPythonPath: getEnvDefault("GARMIN_WRAPPER_PYTHON", "python3"),
|
GarminPythonPath: getEnvDefault("GARMIN_WRAPPER_PYTHON", "python3"),
|
||||||
GarminTokenStoreRoot: getEnvDefault("GARMIN_TOKENSTORE", filepath.Join(filepath.Dir(dbPath), ".garmin")),
|
GarminTokenStoreRoot: getEnvDefault("GARMIN_TOKENSTORE", ".garmin"),
|
||||||
MinConfidence: getEnvFloat("GENIUSRUN_MIN_CONFIDENCE", 0.6),
|
MinConfidence: getEnvFloat("GENIUSRUN_MIN_CONFIDENCE", 0.6),
|
||||||
LogLevel: getEnvDefault("GENIUSRUN_LOG_LEVEL", "info"),
|
LogLevel: getEnvDefault("GENIUSRUN_LOG_LEVEL", "info"),
|
||||||
BackendURL: strings.TrimRight(os.Getenv("GENIUSRUN_BACKEND_URL"), "/"),
|
BackendURL: strings.TrimRight(os.Getenv("GENIUSRUN_BACKEND_URL"), "/"),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -79,7 +78,7 @@ func TestLoad_SessionSecretTooShort(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoad_GarminTokenStoreRootDefaultsNextToDBPath(t *testing.T) {
|
func TestLoad_GarminTokenStoreRootDefaultsIndependentlyOfDBPath(t *testing.T) {
|
||||||
setRequiredEnv(t)
|
setRequiredEnv(t)
|
||||||
t.Setenv("GARMIN_TOKENSTORE", "")
|
t.Setenv("GARMIN_TOKENSTORE", "")
|
||||||
t.Setenv("GENIUSRUN_DB_PATH", "/var/lib/geniusrun/geniusrun.db")
|
t.Setenv("GENIUSRUN_DB_PATH", "/var/lib/geniusrun/geniusrun.db")
|
||||||
@@ -88,9 +87,8 @@ func TestLoad_GarminTokenStoreRootDefaultsNextToDBPath(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Load: %v", err)
|
t.Fatalf("Load: %v", err)
|
||||||
}
|
}
|
||||||
want := filepath.Join("/var/lib/geniusrun", ".garmin")
|
if cfg.GarminTokenStoreRoot != ".garmin" {
|
||||||
if cfg.GarminTokenStoreRoot != want {
|
t.Errorf("GarminTokenStoreRoot = %q, want %q (never derived from DBPath's directory)", cfg.GarminTokenStoreRoot, ".garmin")
|
||||||
t.Errorf("GarminTokenStoreRoot = %q, want %q", cfg.GarminTokenStoreRoot, want)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user