2026-07-17 18:33:06 +02:00
|
|
|
package store
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"database/sql"
|
|
|
|
|
"fmt"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
2026-07-27 06:44:39 +02:00
|
|
|
// SyncKindFull is a manually-triggered "Sync now" pass: backfillCore
|
|
|
|
|
// followed by incrementalSyncCore followed by FillPendingDetails,
|
|
|
|
|
// recorded as one run so the reported activity count covers the whole
|
|
|
|
|
// action instead of only whichever stage happened to finish last.
|
Dedup Garmin data storage, configurable chart colors, taxonomy fixes, sync/progression fixes
- Remove duplicated Garmin fields from storage; decode display-only fields
(activity name/type, lap duration/HR, structured workout raw JSON) from
RawJSON at API-response time instead of storing redundant columns
- Add a fully configurable chart color system (pace/HR main-line colors, 4
effort-kind colors, tint/darken/brighten intensity knobs) under Profile >
Chart colors
- Rename training types and fix their display order (Easy, Long, 60'/30'
Threshold, Tempo, Intervals, MAS Test, Race) everywhere they're listed
- Add an Efficiency Factor progression metric; fix Progression chart axes to
use tight non-zero-based domains, m:ss/km pace formatting, and rounded
ticks instead of raw floating-point labels
- Expose the raw get_workout_by_id() payload in the raw-data viewer
alongside activity/lap/detail JSON; enlarge the modal and shrink array
indentation for readability
- Fix "last sync" reporting a meaningless activity count: record one
combined sync run per manual "Sync now" and count genuinely new
activities instead of re-listing whatever Garmin returned for the queried
window
- Let a Review Queue activity be manually cleared back to Unclassified, and
make "Reset all" available even while disconnected from Garmin
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 06:33:47 +02:00
|
|
|
SyncKindFull = "full"
|
2026-07-17 18:33:06 +02:00
|
|
|
|
|
|
|
|
SyncStatusRunning = "running"
|
|
|
|
|
SyncStatusSuccess = "success"
|
|
|
|
|
SyncStatusError = "error"
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-27 09:01:17 +02:00
|
|
|
// SyncRun records one sync attempt. Every run recorded today is
|
|
|
|
|
// SyncKindFull (the "Sync now" action, covering backfill + incremental sync
|
|
|
|
|
// + detail/workout fill in a single pass) -- "backfill"/"incremental" only
|
|
|
|
|
// ever appear in historical rows predating that consolidation.
|
2026-07-17 18:33:06 +02:00
|
|
|
type SyncRun struct {
|
|
|
|
|
ID int64
|
|
|
|
|
Kind string
|
|
|
|
|
StartedAt string
|
|
|
|
|
FinishedAt *string
|
|
|
|
|
ActivitiesFetched int
|
|
|
|
|
Status string
|
|
|
|
|
ErrorMessage *string
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 17:33:43 +02:00
|
|
|
// StartSyncRun records a new in-progress sync run for userID and returns its id.
|
|
|
|
|
func (db *DB) StartSyncRun(ctx context.Context, userID int64, kind string) (int64, error) {
|
2026-07-17 18:33:06 +02:00
|
|
|
res, err := db.ExecContext(ctx, `
|
2026-07-25 17:33:43 +02:00
|
|
|
INSERT INTO sync_runs (user_id, kind, started_at, status) VALUES (?, ?, datetime('now'), ?)`,
|
|
|
|
|
userID, kind, SyncStatusRunning)
|
2026-07-17 18:33:06 +02:00
|
|
|
if err != nil {
|
2026-07-25 17:33:43 +02:00
|
|
|
return 0, fmt.Errorf("start sync run for user %d: %w", userID, err)
|
2026-07-17 18:33:06 +02:00
|
|
|
}
|
|
|
|
|
return res.LastInsertId()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 17:33:43 +02:00
|
|
|
// FinishSyncRun marks a sync run (owned by userID) as finished, recording
|
|
|
|
|
// how many activities were fetched and whether it succeeded.
|
|
|
|
|
func (db *DB) FinishSyncRun(ctx context.Context, userID, id int64, activitiesFetched int, errMsg *string) error {
|
2026-07-17 18:33:06 +02:00
|
|
|
status := SyncStatusSuccess
|
|
|
|
|
if errMsg != nil {
|
|
|
|
|
status = SyncStatusError
|
|
|
|
|
}
|
|
|
|
|
_, err := db.ExecContext(ctx, `
|
|
|
|
|
UPDATE sync_runs SET finished_at = datetime('now'), activities_fetched = ?, status = ?, error_message = ?
|
2026-07-25 17:33:43 +02:00
|
|
|
WHERE id = ? AND user_id = ?`, activitiesFetched, status, errMsg, id, userID)
|
2026-07-17 18:33:06 +02:00
|
|
|
if err != nil {
|
2026-07-25 17:33:43 +02:00
|
|
|
return fmt.Errorf("finish sync run %d for user %d: %w", id, userID, err)
|
2026-07-17 18:33:06 +02:00
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 17:33:43 +02:00
|
|
|
// LatestSyncRun returns userID's most recent sync run, if any.
|
|
|
|
|
func (db *DB) LatestSyncRun(ctx context.Context, userID int64) (SyncRun, bool, error) {
|
2026-07-17 18:33:06 +02:00
|
|
|
row := db.QueryRowContext(ctx, `
|
|
|
|
|
SELECT id, kind, started_at, finished_at, activities_fetched, status, error_message
|
2026-07-25 17:33:43 +02:00
|
|
|
FROM sync_runs WHERE user_id = ? ORDER BY id DESC LIMIT 1`, userID)
|
2026-07-17 18:33:06 +02:00
|
|
|
var r SyncRun
|
|
|
|
|
err := row.Scan(&r.ID, &r.Kind, &r.StartedAt, &r.FinishedAt, &r.ActivitiesFetched, &r.Status, &r.ErrorMessage)
|
|
|
|
|
if err == sql.ErrNoRows {
|
|
|
|
|
return SyncRun{}, false, nil
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
2026-07-25 17:33:43 +02:00
|
|
|
return SyncRun{}, false, fmt.Errorf("latest sync run for user %d: %w", userID, err)
|
2026-07-17 18:33:06 +02:00
|
|
|
}
|
|
|
|
|
return r, true, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 17:33:43 +02:00
|
|
|
// ListSyncRuns returns userID's recent sync runs, newest first.
|
|
|
|
|
func (db *DB) ListSyncRuns(ctx context.Context, userID int64, limit int) ([]SyncRun, error) {
|
2026-07-17 18:33:06 +02:00
|
|
|
rows, err := db.QueryContext(ctx, `
|
|
|
|
|
SELECT id, kind, started_at, finished_at, activities_fetched, status, error_message
|
2026-07-25 17:33:43 +02:00
|
|
|
FROM sync_runs WHERE user_id = ? ORDER BY id DESC LIMIT ?`, userID, limit)
|
2026-07-17 18:33:06 +02:00
|
|
|
if err != nil {
|
2026-07-25 17:33:43 +02:00
|
|
|
return nil, fmt.Errorf("list sync runs for user %d: %w", userID, err)
|
2026-07-17 18:33:06 +02:00
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
|
|
runs := []SyncRun{}
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var r SyncRun
|
|
|
|
|
if err := rows.Scan(&r.ID, &r.Kind, &r.StartedAt, &r.FinishedAt, &r.ActivitiesFetched, &r.Status, &r.ErrorMessage); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("scan sync run row: %w", err)
|
|
|
|
|
}
|
|
|
|
|
runs = append(runs, r)
|
|
|
|
|
}
|
|
|
|
|
return runs, rows.Err()
|
|
|
|
|
}
|