2026-07-17 18:33:06 +02:00
import { useEffect , useRef , useState } from "react" ;
import { api } from "../api/client" ;
import type { AuthResponse , SyncStatus } from "../types/api" ;
function syncProgressLabel ( status : SyncStatus ) : string {
const { detail_fill_progress : fill , activities_pending_details : pending } = status ;
if ( fill . Total > 0 ) {
// pending includes the current batch, so subtract what's already
// counted in this batch's Total to avoid double-counting the "beyond
// this batch" remainder.
const beyondBatch = Math . max ( 0 , pending - ( fill . Total - fill . Done ) ) ;
return ` syncing: ${ fill . Done } / ${ fill . Total } activities ${ beyondBatch > 0 ? ` (+ ${ beyondBatch } more queued) ` : "" } ` ;
}
return "syncing..." ;
}
export function GarminConnection() {
const [ auth , setAuth ] = useState < AuthResponse | null > ( null ) ;
const [ code , setCode ] = useState ( "" ) ;
const [ busy , setBusy ] = useState ( false ) ;
const [ error , setError ] = useState < string | null > ( null ) ;
const [ syncStatus , setSyncStatus ] = useState < SyncStatus | null > ( null ) ;
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
// 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 ) ;
2026-07-17 18:33:06 +02:00
function refreshStatus() {
api . authStatus ( ) . then ( setAuth ) . catch ( ( e ) = > setError ( String ( e ) ) ) ;
api . syncStatus ( ) . then ( setSyncStatus ) . catch ( ( ) = > { } ) ;
}
// Poll faster while a sync is actually running, so progress feels live;
// back off to a relaxed interval the rest of the time.
const syncStatusRef = useRef ( syncStatus ) ;
syncStatusRef . current = syncStatus ;
useEffect ( ( ) = > {
refreshStatus ( ) ;
let timeout : ReturnType < typeof setTimeout > ;
const tick = ( ) = > {
refreshStatus ( ) ;
timeout = setTimeout ( tick , syncStatusRef . current ? . in_progress ? 1500 : 6000 ) ;
} ;
timeout = setTimeout ( tick , 1500 ) ;
return ( ) = > clearTimeout ( timeout ) ;
} , [ ] ) ;
async function connect() {
setBusy ( true ) ;
setError ( null ) ;
try {
setAuth ( await api . login ( ) ) ;
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
setDisconnected ( false ) ;
2026-07-17 18:33:06 +02:00
} catch ( e ) {
setError ( String ( e ) ) ;
} finally {
setBusy ( false ) ;
}
}
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
function disconnect() {
setDisconnected ( true ) ;
}
2026-07-17 18:33:06 +02:00
async function submitMFA() {
if ( ! code . trim ( ) ) return ;
setBusy ( true ) ;
setError ( null ) ;
try {
setAuth ( await api . submitMFA ( code . trim ( ) ) ) ;
setCode ( "" ) ;
} catch ( e ) {
setError ( String ( e ) ) ;
} finally {
setBusy ( false ) ;
}
}
2026-07-19 12:41:38 +02:00
async function sync() {
2026-07-17 18:33:06 +02:00
setBusy ( true ) ;
setError ( null ) ;
try {
2026-07-19 12:41:38 +02:00
await api . syncRun ( ) ;
refreshStatus ( ) ;
} catch ( e ) {
setError ( String ( e ) ) ;
} finally {
setBusy ( false ) ;
}
}
async function resetAll() {
if ( ! window . confirm ( "This deletes every synced activity, lap, and kind assignment, then starts a fresh pull from Garmin next sync. This cannot be undone. Continue?" ) ) {
return ;
}
setBusy ( true ) ;
setError ( null ) ;
try {
await api . resetSync ( ) ;
2026-07-19 12:45:03 +02:00
// Reset runs as a background sync job; wait for it to actually finish
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
// before reloading, otherwise other pages (e.g. Activities) would
2026-07-19 12:45:03 +02:00
// still show the just-deleted activities from their own stale state.
let status = await api . syncStatus ( ) ;
while ( status . in_progress ) {
await new Promise ( ( resolve ) = > setTimeout ( resolve , 300 ) ) ;
status = await api . syncStatus ( ) ;
}
window . location . reload ( ) ;
2026-07-17 18:33:06 +02:00
} catch ( e ) {
setError ( String ( e ) ) ;
setBusy ( false ) ;
}
}
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
const status = disconnected ? "unknown" : auth ? . status ? ? "unknown" ;
2026-07-17 18:33:06 +02:00
return (
< div className = "garmin-connection" >
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
< 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" && (
< >
< button disabled = { busy } onClick = { sync } >
Sync now
< / button >
< button disabled = { busy } onClick = { disconnect } >
Disconnect
< / button >
< / >
) }
{ / * R e s e t a l l o n l y w i p e s l o c a l D B s t a t e ( s e e h a n d l e S y n c R e s e t ) - - i t
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
2026-07-17 18:33:06 +02:00
< / button >
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
< / div >
2026-07-17 18:33:06 +02:00
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
< div className = "garmin-connection-status" >
< span className = { ` status-dot status- ${ status } ` } / >
{ / * C o n n e c t e d / n o t - c o n n e c t e d i s a l r e a d y c o n v e y e d b y t h e
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 >
2026-07-17 18:33:06 +02:00
< / div >
{ status === "mfa_required" && (
< div className = "garmin-connection-row" >
< input
placeholder = "MFA code"
value = { code }
onChange = { ( e ) = > setCode ( e . target . value ) }
onKeyDown = { ( e ) = > e . key === "Enter" && submitMFA ( ) }
/ >
< button disabled = { busy } onClick = { submitMFA } >
Submit code
< / button >
< / div >
) }
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
{ ! disconnected && auth ? . message && < p className = "garmin-connection-message" > { auth . message } < / p > }
{ / * F u l l - w i d t h s o a l o n g " l a s t s y n c " / p e n d i n g - d e t a i l s m e s s a g e h a s r o o m
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 >
) }
2026-07-17 18:33:06 +02:00
{ error && < p className = "error" > { error } < / p > }
< / div >
) ;
}