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:
@@ -20,6 +20,10 @@ export function GarminConnection() {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
|
||||
// There's no real Garmin logout -- the backend session just sits idle.
|
||||
// "Disconnect" only hides that and shows Connect again locally; a real
|
||||
// login (connect()) clears it.
|
||||
const [disconnected, setDisconnected] = useState(false);
|
||||
|
||||
function refreshStatus() {
|
||||
api.authStatus().then(setAuth).catch((e) => setError(String(e)));
|
||||
@@ -47,6 +51,7 @@ export function GarminConnection() {
|
||||
setError(null);
|
||||
try {
|
||||
setAuth(await api.login());
|
||||
setDisconnected(false);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
@@ -54,6 +59,10 @@ export function GarminConnection() {
|
||||
}
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
setDisconnected(true);
|
||||
}
|
||||
|
||||
async function submitMFA() {
|
||||
if (!code.trim()) return;
|
||||
setBusy(true);
|
||||
@@ -90,7 +99,7 @@ export function GarminConnection() {
|
||||
try {
|
||||
await api.resetSync();
|
||||
// Reset runs as a background sync job; wait for it to actually finish
|
||||
// before reloading, otherwise other pages (e.g. Review Queue) would
|
||||
// before reloading, otherwise other pages (e.g. Activities) would
|
||||
// still show the just-deleted activities from their own stale state.
|
||||
let status = await api.syncStatus();
|
||||
while (status.in_progress) {
|
||||
@@ -104,45 +113,48 @@ export function GarminConnection() {
|
||||
}
|
||||
}
|
||||
|
||||
const status = auth?.status ?? "unknown";
|
||||
const status = disconnected ? "unknown" : auth?.status ?? "unknown";
|
||||
|
||||
return (
|
||||
<div className="garmin-connection">
|
||||
<div className="garmin-connection-row">
|
||||
<span className={`status-dot status-${status}`} />
|
||||
<span className="status-label">
|
||||
{status === "authenticated" && "Connected to Garmin"}
|
||||
{status === "mfa_required" && "MFA code required"}
|
||||
{status === "failed" && "Connection failed"}
|
||||
{status === "unknown" && "Not connected to Garmin"}
|
||||
</span>
|
||||
<div className="garmin-connection-row garmin-connection-main">
|
||||
<div className="garmin-connection-actions">
|
||||
{status !== "authenticated" && status !== "mfa_required" && (
|
||||
<button disabled={busy} onClick={connect}>
|
||||
Connect to Garmin
|
||||
</button>
|
||||
)}
|
||||
|
||||
{status !== "authenticated" && status !== "mfa_required" && (
|
||||
<button disabled={busy} onClick={connect}>
|
||||
Connect to Garmin
|
||||
{status === "authenticated" && (
|
||||
<>
|
||||
<button disabled={busy} onClick={sync}>
|
||||
Sync now
|
||||
</button>
|
||||
<button disabled={busy} onClick={disconnect}>
|
||||
Disconnect
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Reset all only wipes local DB state (see handleSyncReset) -- it
|
||||
doesn't touch the Garmin session, so it stays available even
|
||||
while disconnected/not-yet-connected. */}
|
||||
<button className="button-danger" disabled={busy} onClick={resetAll}>
|
||||
Reset all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{status === "authenticated" && (
|
||||
<>
|
||||
<button disabled={busy} onClick={sync}>
|
||||
Sync now
|
||||
</button>
|
||||
<button className="button-danger" disabled={busy} onClick={resetAll}>
|
||||
Reset all
|
||||
</button>
|
||||
{syncStatus?.in_progress && (
|
||||
<span className="status-label">{syncProgressLabel(syncStatus)}</span>
|
||||
)}
|
||||
{syncStatus?.last_run && !syncStatus.in_progress && (
|
||||
<span className="status-label">
|
||||
last sync: {syncStatus.last_run.Status} ({syncStatus.last_run.ActivitiesFetched} activities)
|
||||
{syncStatus.activities_pending_details > 0 &&
|
||||
` — ${syncStatus.activities_pending_details} still need details, click Sync now again`}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="garmin-connection-status">
|
||||
<span className={`status-dot status-${status}`} />
|
||||
{/* Connected/not-connected is already conveyed by the
|
||||
Connect/Disconnect button itself -- only MFA/failed need a
|
||||
label, since no button distinguishes those from "not connected". */}
|
||||
{(status === "mfa_required" || status === "failed") && (
|
||||
<span className="status-label">
|
||||
{status === "mfa_required" ? "MFA code required" : "Connection failed"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{status === "mfa_required" && (
|
||||
@@ -159,7 +171,21 @@ export function GarminConnection() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{auth?.message && <p className="garmin-connection-message">{auth.message}</p>}
|
||||
{!disconnected && auth?.message && <p className="garmin-connection-message">{auth.message}</p>}
|
||||
|
||||
{/* Full-width so a long "last sync"/pending-details message has room
|
||||
to breathe, instead of being squeezed next to the status dot. */}
|
||||
{status === "authenticated" && syncStatus?.in_progress && (
|
||||
<p className="garmin-connection-message">{syncProgressLabel(syncStatus)}</p>
|
||||
)}
|
||||
{status === "authenticated" && syncStatus?.last_run && !syncStatus.in_progress && (
|
||||
<p className="garmin-connection-message">
|
||||
last sync: {syncStatus.last_run.Status} ({syncStatus.last_run.ActivitiesFetched} activities)
|
||||
{syncStatus.activities_pending_details > 0 &&
|
||||
` — ${syncStatus.activities_pending_details} still need details, click Sync now again`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{error && <p className="error">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user