Commit Graph

237 Commits

Author SHA1 Message Date
Davide Scaini d4e5b11f71 admin: add Total imported and Last sync columns to Garmin sync table
Matches the Strava sync table layout. Accumulates total_imported in
garmin_sync.json state on each sync run; admin API exposes last_sync_at
and total_imported from that file.
2026-05-21 20:34:25 +02:00
Davide Scaini 29c6e399c0 perf: skip feed index fetch when navigating from activity feed
Write the activity summary to sessionStorage on click in ActivityFeed,
then read it synchronously at module init in ActivityDetailLoader so the
page renders immediately without the "Loading activity…" blank screen
or the 2 round-trip index fetch.

Direct URL / bookmark / shared link falls through to the existing slow
path unchanged.
2026-05-19 19:44:20 +02:00
Davide Scaini bbfab72138 ActivityCharts: switch slope tab to distance-weighted smoothing
Replace time-based rolling mean with a 400 m distance-weighted sliding-
window average (O(n), two advancing pointers) matching the spec in
SLOPE_COLORING.md. Slope values are now spatially consistent regardless
of riding speed. Smoothing buttons are hidden when the slope tab is
active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 19:03:56 +02:00
Davide Scaini 6faf63c2cd ActivityCharts: add slope coloring tab to elevation profile
New "Slope" tab colours the filled area and stroke line with an SVG
linearGradient driven by per-point slope data (green→yellow→orange→red→
purple scale). Slope is computed from smoothed elevation + cumulative
distance, reusing the existing raw/10s/20s smoothing controls. The
hover tooltip shows slope % (in slope colour) and elevation. Tab is
enabled only when both elevation and distance data are present; the
X-mode and histogram toggles are hidden for this tab.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:57:55 +02:00
Davide Scaini 766da0320b NerdCorner VAM: filter short climbs, opacity-encode confidence, add climbing time to tooltip
- Exclude per-activity VAM contributions where climbing_time_s < 10 min; short
  punchy efforts don't represent aerobic fitness and were skewing monthly averages
- Store climbing_time_s alongside climbing_vam_mh in metrics, detail JSON, and
  summary JSON so the frontend has the data to reason about confidence
- Accumulate total climbing time per period; opacity scales from 0.25 (10 min,
  minimum threshold) to 1.0 (≥ 1 h) so thin-evidence months read as faint dots
- Render VAM as dots only (no lines) since each period is an independent average,
  not a cumulative — lines implied continuity that isn't there
- Tooltip now shows "1060 m/h · 38 min climbing"
2026-05-17 10:13:39 +02:00
Davide Scaini 7a44cbbef0 change segment width and opacity 2026-05-17 09:51:11 +02:00
Davide Scaini 979a6c527f Segment create: load existing segments in activity bbox as reference layer 2026-05-17 09:40:41 +02:00
Davide Scaini 6bc77486f1 Segments: color track green→red along direction of travel 2026-05-17 09:32:31 +02:00
Davide Scaini 9521a64da4 Activity stats: fixed-position pairs so optional values don't shift layout 2026-05-17 09:15:11 +02:00
Davide Scaini 7953e05241 Fix build: use class ternary instead of class: directive for Tailwind /opacity class 2026-05-17 09:06:04 +02:00
Davide Scaini db9b4ce32c Activity map: click stat to lock track color mode, hover still previews 2026-05-17 09:01:59 +02:00
Davide Scaini 14a4a0b994 Activity detail: layout refactor + GPS-derived speed for map coloring
Layout: map + charts stacked left, stats panel (2-col) on the right.
Cadence moved to last stat. Charts sit directly below the map.

Speed coloring: most FIT files don't record per-second speed, leaving
timeseries speed_kmh all-null and the hover link dead. Fix: derive speed
from consecutive GPS coordinates (haversine + 5-pt moving average) when
the device didn't record it. Add --backfill-speed render flag to retrofit
existing timeseries files.
2026-05-16 23:24:29 +02:00
Davide Scaini 0dc450ba30 Fix track coloring hover: inline reactive vars so Svelte tracks deps
statColorMode() hid hasSpeedTrack etc. from Svelte's compiler so the
{#each} block never re-rendered when timeseries loaded. Inline the
ternary directly so all reactive variables are visible to the tracker.
2026-05-16 22:56:20 +02:00
Davide Scaini 1cca485062 Activity map: extend track coloring to HR, power, elevation, cadence
Refactor the metric gradient code into shared _computeProgress +
_buildGradient helpers. Hovering any of speed/HR/power/elevation/cadence
stats switches the track to a blue→green→yellow→red gradient for that
metric. A small legend (min ·gradient bar· max + label) appears in the
bottom-left corner of the map while active. Absolute elevation used
(not slope), so blue=valleys, red=peaks.
2026-05-16 22:50:24 +02:00
Davide Scaini f71fe2ddf5 Activity map: color track by speed when hovering speed stats
Hovering avg/max speed in the stats panel switches the track from the
default gradient to a blue→green→yellow→red speed gradient. Progress
along the track is computed from cumulative distance (speed × time) so
fast sections appear proportionally long rather than time-weighted.
Reverts to default on mouse-leave. No-op when timeseries has no speed
data or the activity has no GPS track.
2026-05-16 22:43:12 +02:00
Davide Scaini 08e8e54c36 Power curve: show record holder in tooltip and add records table
Find the activity that holds each MMP record by scanning per-activity
mmp arrays. Activity title appears in the chart hover tooltip. A table
below the chart lists every duration with the record watts, activity
title (linked), and date. The table has its own all-time/365d/90d toggle
independent of the chart overlays.
2026-05-16 22:25:30 +02:00
Davide Scaini 003b540481 VAM: drop duration curve, show avg climbing VAM in Nerd Corner
Remove the per-duration VAM curve everywhere (metrics, summaries, detail
JSON, athlete.json, VamChart.svelte, AthleteView VAM tab). Keep only
climbing_vam_mh per activity. Add it to activity summaries so NerdCorner
can plot average climbing VAM per week/month year-over-year alongside
distance/elevation/time. Add --backfill-vam-summary flag to copy the
field from existing detail JSONs into index.json without re-extracting.
2026-05-16 22:03:40 +02:00
Davide Scaini baf20b51ba Add VAM (climbing velocity) metric and per-duration curve
Extract pipeline now computes two VAM metrics per activity (cycling,
running, hiking, walking):
- climbing_vam_mh: VAM on ascending segments only, using 30 s forward
  lookahead to classify climbing vs. flat/descent (stored in detail JSON)
- vam_curve: [[duration_s, vam_mh], ...] best VAM per standard duration
  (60 s – 1 h), sliding window on 30 s smoothed elevation, only windows
  with ≥ 10 m net gain count (stored in summary + detail)

Athlete JSON aggregates vam_curve across all activities (all_time,
last_365d, last_90d), same structure as power_curve.

Frontend:
- ActivityDetail shows "Climbing VAM" stat (grouped with elevation)
- AthleteView adds a "VAM Curve" tab that appears only when the athlete
  has climbing data; renders VamChart (new component, mirrors MmpChart)

vam_curve stripped from combined global feed; kept in user year shards
for season-based on-the-fly aggregation in VamChart.

Requires bincio reextract to backfill existing activities.
2026-05-16 21:34:06 +02:00
Davide Scaini de602ff5d9 Settings: per-user default for download_disabled
New pref download_disabled_default (stored in user_prefs + mirrored to
_user_settings.json for the render pipeline). When true, apply_sidecar
marks all activities as download_disabled unless the sidecar explicitly
sets download_disabled: false (per-activity opt-in from the edit drawer).

Settings page gets an "Activity defaults" card with the toggle.
2026-05-16 20:51:23 +02:00
Davide Scaini 2d9620c6d1 Admin: add Garmin sync status panel
New /api/admin/garmin-sync (GET) and /api/admin/garmin-sync/run (POST)
endpoints mirror the Strava equivalents, reading _garmin_sync_status.json
per user and exposing a run-now button. Admin page shows the Garmin table
below the Strava one, with auth_error/api_error/ok badges and live polling
while a sync is running.
2026-05-16 20:31:02 +02:00
Davide Scaini 2c69e75842 Show orange upload button when Strava/Garmin auth fails
GET /api/me/sync-status reads _strava_sync_status.json and
_garmin_sync_status.json for the logged-in user. On page load the nav
script checks this endpoint and, if either service has status=auth_error,
turns the upload arrow orange with a tooltip naming the disconnected
service(s).
2026-05-16 20:27:43 +02:00
Davide Scaini c465e518e5 Add activity file downloads with per-activity download_disabled flag
New endpoint: GET /api/activity/{id}/download/{bas|original|gpx}
- bas: streams the BAS detail JSON as an attachment
- original: streams the original FIT or GPX file from originals/
- gpx: generates a GPX from the timeseries (always available when GPS exists)

download_disabled flag stored in sidecar (edits/{id}.md), propagated to
the merged BAS detail JSON. When set, only the owner can download.

Backend: ops.py writes flag to sidecar; merge.py propagates it to detail
JSON; download.py implements the endpoint; server.py registers the router.
Frontend: EditDrawer gets a "No download" toggle button; ActivityDetail
shows a Download section (hidden when disabled and viewer is not the owner).
2026-05-15 18:35:40 +02:00
Davide Scaini fe437626e6 Global feed: switch from sequential pages to month-based BAS shards
feed.json is now a BAS shard index pointing to feed-YYYY-MM.json files
(~150 activities / ~25 KB gzip each) instead of 400+ sequential feed-N.json
pages. The frontend can now jump directly to a specific month when filtering
by year or date range, without loading all newer data first.

- merge.py: write_combined_feed groups by YYYY-MM and emits a shard index
- dataloader.ts: isYearShardUrl matches feed-YYYY-MM.json; loadCombinedFeed
  returns pendingShards; FeedPage interface and loadCombinedFeedPage removed
- ActivityFeed.svelte: _yearFromShard handles both index-YYYY and feed-YYYY-MM;
  feedNextPage/feedTotalPages/loadingAllFeedPages removed; infinite-loop bug
  fixed (toLoad.length guard before setting loadingAllShards); onMount uses
  pendingShards from loadCombinedFeed
2026-05-15 10:25:01 +02:00
Davide Scaini d3bce49445 Feed: eager-load only the year shards needed for the active date filter 2026-05-15 09:32:12 +02:00
Davide Scaini 8a06227243 Feed date filter: early-stop global feed load, fix cross-date validation, show Loading while fetching
- Stop fetching combined-feed pages once the oldest activity in a batch predates
  the from-date (feed is newest-first, so everything needed is already loaded)
- Show "Loading…" instead of "No activities found" while eager-load is in progress
- Constrain From max to customTo (or today) and To min to customFrom so the
  range can't be inverted via the date pickers
2026-05-15 09:24:02 +02:00
Davide Scaini 1f3f5b3d3b Feed: fix date range eager-load — use primary let-vars, cover feed pages
Previous attempt used dateFrom (a derived $: variable) as the trigger which
Svelte 5 doesn't reliably track as a dependency of a side-effect $: block.
Replace with the primary let-variables (customFrom, customTo, datePre) that
Svelte does track statically.

Also extend eager-loading to cover the global combined feed (feedNextPage)
so date/search filtering works on multi-user instances too, not just per-user
profile pages (pendingShards).
2026-05-15 09:14:56 +02:00
Davide Scaini d2151a4acf Ideas: add reopen button when awaiting; add /reopen endpoint 2026-05-15 09:07:49 +02:00
Davide Scaini 9cc70269f5 Feed: eager-load all year shards when a date filter is active
The initial page load only fetches the most recent year shard. Selecting a
date range or year preset that spans an older shard returned no results because
those shards were never loaded. Extend the existing search eager-load trigger
to also fire on any non-empty dateFrom, covering both custom date inputs and
year preset buttons.
2026-05-15 08:59:06 +02:00
Davide Scaini afbcaa5011 Feed: cap date range inputs at today to prevent selecting future dates 2026-05-15 08:38:58 +02:00
Davide Scaini 15e9969ca2 Ideas: add 'won't implement' status with decline/reopen button 2026-05-15 08:36:31 +02:00
Davide Scaini c905449114 Feed: add custom date range (From/To) inputs alongside search bar 2026-05-15 08:32:31 +02:00
Davide Scaini ed6a7ed39c Ideas: add 'awaiting feedback' status with amber section + admin comment
Status cycles open → awaiting → done → reopen.
Awaiting ideas float to the top in a 'Waiting for your feedback' section
with an amber border (#f59e0b).

Admin can attach an implementation note to any awaiting idea via
POST /api/ideas/{id}/comment. The note appears inside the same card
in a distinct sub-box with a subtle amber tint border, editable inline.
The sub-box is visible to all users once a note exists.
2026-05-15 08:18:44 +02:00
Davide Scaini c12f5336f5 AthleteView: listen for bincio:me event so owner tabs appear without hard refresh
In multi-user instances /api/me is async and usually hasn't returned by the
time onMount runs, leaving isOwner=false. Subscribe to the bincio:me event
(fired by Base.astro when /api/me resolves) so the reactive TABS filter
re-evaluates and Explore / Nerd Corner appear without needing Cmd+Shift+R.
2026-05-15 01:15:24 +02:00
Davide Scaini 0fbb7822df EditDrawer: show sub_sport badge immediately after save without page reload 2026-05-15 00:52:34 +02:00
Davide Scaini a863cdd663 Records: exclude Zwift activities by title; show Saved confirmation before closing drawer
- _is_outdoor now also excludes activities whose title matches /\bzwift\b/i,
  covering the ~50 Strava-imported Zwift rides that lack sub_sport metadata.

- EditDrawer waits 900ms after a successful save before dispatching 'saved'
  (which closes the drawer), so the green "Saved" confirmation is visible.
2026-05-15 00:49:46 +02:00
Davide Scaini b509db4940 nerd corner: cool-to-warm year color ramp (proposal, not pushed) 2026-05-14 18:47:08 +02:00
Davide Scaini 653db2428f nerd corner: add cumulative plot below the per-period chart 2026-05-14 18:43:05 +02:00
Davide Scaini 5167f2a988 explore: shard tracks into per-year files for progressive loading
bake_tracks now writes tracks_YYYY.json shards + tracks_index.json manifest
instead of a single monolithic tracks.json. API /api/me/tracks returns the
manifest; /api/me/tracks/{year} serves individual shards. Explore.svelte
fetches the two most recent years eagerly then streams the rest in the
background so the map renders immediately with recent data.
2026-05-14 18:34:53 +02:00
Davide Scaini 8af6b7b04e nav: always show upload button on all screen sizes 2026-05-14 18:24:44 +02:00
Davide Scaini 16965a7645 ActivityDetail: fetch timeseries in parallel with detail JSON to cut load time 2026-05-14 18:22:05 +02:00
Davide Scaini 4d6859b927 NerdCorner: show per-period load instead of cumulative 2026-05-14 16:42:16 +02:00
Davide Scaini b32553b0b1 Fix NerdCorner: pass all activities, not only those with power data 2026-05-14 16:39:57 +02:00
Davide Scaini 8804bdec37 Add Nerd Corner tab with year-over-year cumulative progress chart 2026-05-14 16:37:01 +02:00
Davide Scaini 487ce42361 Explore: type pill dark/light theme split; freeze active pill on hover in dark theme 2026-05-14 16:24:00 +02:00
Davide Scaini 46445dd1cb Move Invites link from athlete page to settings; type pill active state contrast fix 2026-05-14 16:22:31 +02:00
Davide Scaini ab112788b4 Explore: grey pill background dark-theme only; transparent in light mode 2026-05-14 16:18:10 +02:00
Davide Scaini 8d799e8e64 Explore: active type pill solid color bg with auto black/white text contrast 2026-05-14 16:17:16 +02:00
Davide Scaini cfb7198d64 Explore: raise inactive type-pill background opacity to 70% for dark theme visibility 2026-05-14 16:11:48 +02:00
Davide Scaini 2b9e080b4c Explore: opacity slider heatmap-only; lines mode width-only at 100% opacity 2026-05-14 16:07:57 +02:00
Davide Scaini 20bb5bfb60 explore: grey background on inactive type pills for readability 2026-05-14 15:59:00 +02:00