Commit Graph

235 Commits

Author SHA1 Message Date
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
Davide Scaini dc719a55d5 explore: show width/opacity sliders in lines mode too 2026-05-14 15:57:20 +02:00
Davide Scaini 5593764fdb explore: skip legacy bare-timestamp geojsons; type pill colors visible when inactive 2026-05-14 15:55:10 +02:00