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>
This commit is contained in:
2026-07-20 06:33:47 +02:00
parent 9818d35910
commit 518bccf5ab
56 changed files with 2334 additions and 890 deletions

View File

@@ -1,24 +1,31 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { api } from "./api/client";
import "./App.css";
import { GarminConnection } from "./components/GarminConnection";
import { Activities } from "./pages/Activities";
import { Dashboard } from "./pages/Dashboard";
import { Plan } from "./pages/Plan";
import { Profile } from "./pages/Profile";
import { ReviewQueue } from "./pages/ReviewQueue";
import { WorkoutKinds } from "./pages/WorkoutKinds";
const TABS = [
{ key: "dashboard", label: "Progression", Component: Dashboard },
{ key: "review", label: "Review Queue", Component: ReviewQueue },
{ key: "activities", label: "Activities", Component: Activities },
{ key: "kinds", label: "Workout Kinds", Component: WorkoutKinds },
{ key: "profile", label: "Profile", Component: Profile },
{ key: "activities", label: "Activities", icon: "☰", Component: ReviewQueue },
{ key: "dashboard", label: "Progression", icon: "↗", Component: Dashboard },
{ key: "plan", label: "Training plan", icon: "✎", Component: Plan },
] as const;
type TabKey = (typeof TABS)[number]["key"];
function App() {
const [tab, setTab] = useState<TabKey>("dashboard");
const [tab, setTab] = useState<TabKey>("activities");
// Profile isn't a tab: it's reached via the profile name in the top-right
// corner instead, since (for now, single-profile) it's account settings,
// not a content view alongside Activities/Progression/Training plan.
const [showProfile, setShowProfile] = useState(false);
const [profileName, setProfileName] = useState<string | null>(null);
useEffect(() => {
api.getProfile().then((p) => setProfileName(p.Name)).catch(() => {});
}, []);
const Active = TABS.find((t) => t.key === tab)!.Component;
return (
@@ -29,18 +36,26 @@ function App() {
{TABS.map((t) => (
<button
key={t.key}
className={t.key === tab ? "tab active" : "tab"}
onClick={() => setTab(t.key)}
className={!showProfile && t.key === tab ? "tab active" : "tab"}
onClick={() => {
setShowProfile(false);
setTab(t.key);
}}
>
<span className="tab-icon">{t.icon}</span>
{t.label}
</button>
))}
</nav>
<button
type="button"
className={showProfile ? "profile-name active" : "profile-name"}
onClick={() => setShowProfile(true)}
>
{profileName ?? "Profile"}
</button>
</header>
<GarminConnection />
<main>
<Active />
</main>
<main>{showProfile ? <Profile onSaved={(p) => setProfileName(p.Name)} /> : <Active />}</main>
</div>
);
}