Commit Graph

110 Commits

Author SHA1 Message Date
Davide Scaini e7228c2be8 explore: plasma palette; width + opacity sliders for heatmap 2026-05-14 15:48:30 +02:00
Davide Scaini 298fe3ea39 explore: fix by-type layer visibility — only show selected types 2026-05-14 15:43:12 +02:00
Davide Scaini 4e32cf4f21 explore: grey map default; zoom-scaled heatmap lines; fix all/none type buttons 2026-05-14 15:35:40 +02:00
Davide Scaini a75fabecb9 explore: thicker heatmap lines (4px + blur) 2026-05-14 14:57:39 +02:00
Davide Scaini b3c41967f6 explore: replace gaussian heatmap with line-accumulation (strava-style) 2026-05-14 14:54:47 +02:00
Davide Scaini 6d13993f98 explore: default heatmap/by-type; month All button; bbox filtering on map move 2026-05-14 14:45:54 +02:00
Davide Scaini 5307ae287c Explore: personal GPS heatmap tab under Athlete page
- bincio/explore.py: bake_tracks() simplifies GPS coords (RDP ε=0.0001),
  strips to [lng,lat], groups by sport type, writes per-handle tracks.json
- bake-tracks CLI command; render CLI calls _bake_tracks() after each build;
  strava_zip runs it once at end of batch
- /api/me/tracks endpoint serves the baked file; wipe_user cleans it up
- Explore.svelte: MapLibre full-screen map with sidebar — type pills,
  year/month date filter, Lines / Heatmap (global or by-type) view modes
- AthleteView: Explore tab visible only to profile owner (checks __bincioMe)
- Base.astro: fullscreen prop + Planner nav link
2026-05-14 14:31:21 +02:00
Davide Scaini fb033e3da2 Search: auto-load all year shards when a query is entered so full history is searched 2026-05-13 21:17:55 +02:00
Davide Scaini b9a21e8bcc ideas: add inline edit for own ideas (author + admin) 2026-05-13 19:52:25 +02:00
Davide Scaini aa1c0b38c0 ideas: add Feedback button to header 2026-05-13 19:48:34 +02:00
Davide Scaini c2c4cb9f3a segments: fit initial map view to all existing segments 2026-05-13 19:41:59 +02:00
Davide Scaini d82033fd84 ideas: update bug report link text 2026-05-13 19:34:02 +02:00
Davide Scaini c30a15d295 ideas: add done/reopen status toggle for admins
Admin-only POST /api/ideas/{id}/status toggles status between open and
done. Done ideas are greyed out (opacity 0.55), show a green checkmark,
and sink to the bottom of the list. Admins see done/reopen buttons on
each card.
2026-05-13 19:32:30 +02:00
Davide Scaini 38f2e51788 ideas: add Ideas page, nav link; remove feedback button from About
New /ideas/ page with Svelte component: card list sorted by votes,
inline submit form, optimistic vote toggling, delete for own/admin.
Bug report link moved to bottom of Ideas page. Feedback button removed
from About page.
2026-05-13 19:29:39 +02:00
Davide Scaini cb3c9b6e41 Move search bar above sport/date filters, below page title 2026-05-13 11:54:37 +02:00
Davide Scaini 861748a059 ActivityFeed: add title search bar with URL sync 2026-05-13 11:51:14 +02:00
Davide Scaini f00e5e47b2 SegmentDetail: sort efforts by time by default, sortable column headers 2026-05-13 11:17:22 +02:00
Davide Scaini 0ff5473dfd Athlete segments tab: link best time to activity; expandable effort list
- best_activity_id now included in segment_summary API response
- Best time is a direct link to the activity that produced it
- Clicking a row expands an inline effort list (lazy-loaded from
  /api/segments/{id}/efforts): date linked to activity, time, Δ vs PR
- Clicking again collapses; ▲/▼ chevron shows state
2026-05-13 08:40:39 +02:00
Davide Scaini 59cf99f0af Fix stuck segments tab; add /segments/ dev fallback
AthleteView: use segmentsFetched flag to prevent infinite fetch loop when
there are no efforts (segmentSummary.length === 0 was re-triggering the
reactive statement after every empty response). Also improve empty state
message and reset flag after rescan so the table reloads.

astro.config.mjs: extend shell fallback plugin to cover /segments/{id}/
the same way /activity/{id}/ is handled, so segment detail pages work in
the dev server without nginx.
2026-05-13 08:35:00 +02:00
Davide Scaini b8fd4e4ded Move segment rescan button from segments list to athlete/segments tab 2026-05-13 08:20:05 +02:00
Davide Scaini d7fd585e77 Add global segment rescan: POST /api/me/segment-rescan + Rescan all button 2026-05-13 08:17:18 +02:00
Davide Scaini f2075e29d2 Segments Phase 4: detail page, activity efforts, athlete tab, new APIs
New API endpoints:
- GET /api/segments/{id} — single segment metadata
- GET /api/activities/{id}/segment_efforts — efforts for an activity (auth)
- GET /api/users/{handle}/segment_summary — public best time + count per segment

New components:
- SegmentDetail.svelte — map + metadata + effort table (with PR/Δ) + rescan button
- SegmentsPage.svelte — URL router: shows detail when /segments/{id}/, list otherwise

Updated:
- segments/index.astro — now uses SegmentsPage router
- nginx-activity.conf — add /segments/ try_files rule for client-side routing
- ActivityDetail.svelte — segment efforts block below laps
- AthleteView.svelte — Segments tab with best time + effort count per segment
- format.ts — add formatElapsed() for compact m:ss display
2026-05-13 08:09:24 +02:00
Davide Scaini c7f0013e57 SegmentCreate: prompt after save instead of immediate redirect; update plan
After saving, show "Saved! Add another from this activity?" with two
buttons: "Add another" (resets name/handles, keeps map loaded) and
"Done" (navigates to /segments/).
2026-05-13 01:03:34 +02:00
Davide Scaini 6c9de35426 Enforce 500 m minimum segment length in UI and API 2026-05-13 00:56:04 +02:00
Davide Scaini e9e7b5d0e7 SegmentCreate: add elevation profile that zooms to selected portion
Shows a dim area for the visible range around the selection (4% padding)
and a blue overlay for the selected segment, with a light stroke on the
top edge. Both the x-domain and y-domain track the selection, so the
chart zooms in as the handles narrow. Elevation min/max labels overlaid
at top-left and bottom-left.
2026-05-13 00:54:39 +02:00
Davide Scaini 61db0734d2 Move segment shortcut next to Edit button, shorten to '+ segment' 2026-05-13 00:39:51 +02:00
Davide Scaini dd9f7a82dc Segments phase 2: /segments/ browse page, /segments/new/ creation flow, activity detail shortcut 2026-05-13 00:36:44 +02:00
Davide Scaini 6b2698c0c5 Mark fallback NP computation for future removal 2026-05-12 23:52:19 +02:00
Davide Scaini c46e91d0f5 Compute NP from timeseries in frontend for activities missing np_power_w in JSON 2026-05-12 23:51:22 +02:00
Davide Scaini bd0595ee79 Add avg power and NP to activity summary; NP uses Coggan 30s rolling-average method 2026-05-12 23:47:06 +02:00
Davide Scaini f1fec6d825 ActivityCharts: smoothing toggle (Raw/10s/20s) for all line chart metrics 2026-05-12 23:37:41 +02:00
Davide Scaini a5db6142b3 ActivityCharts: 10s rolling mean on cadence and power line charts (display only) 2026-05-12 23:32:33 +02:00
Davide Scaini 1298586a74 ActivityCharts: extend reference lines to HR; use high-contrast label styling 2026-05-12 23:29:09 +02:00
Davide Scaini 3231fdb4b7 ActivityCharts: add avg/P20/P80 reference lines to speed, cadence, and power line charts 2026-05-12 23:24:33 +02:00
Davide Scaini 867da767eb Add sub_sport editing to activity edit drawer 2026-05-12 23:01:12 +02:00
Davide Scaini 680ef9d440 Hide edit controls on activities the logged-in user does not own 2026-05-03 18:51:52 +02:00
Davide Scaini f6e9fe8198 feat(serve): debounced site rebuild — burst uploads trigger one build, not N
Replace per-upload Astro build threads with a single background worker
(_site_rebuild_worker) that waits on an event, sleeps 60 s to let upload
bursts settle, then runs one full build + rsync. 271 concurrent uploads now
produce one build instead of 271 serialised builds, eliminating the OOM kill.
--webroot is re-enabled; merge-only path still runs immediately per upload.

Also: date filter row added to ActivityFeed.svelte (sport + date presets
with dynamic year pills); deploy/vps gitignored for VPS config backups.
2026-04-30 21:23:29 +02:00
Davide Scaini ebac3f50f4 fix: DEM elevation overcounting and add hysteresis-only recalculation button
- dem.py: apply 45s median filter before hysteresis to suppress SRTM
  tile-boundary steps that were accumulating through the 5m threshold;
  raise DEM hysteresis threshold from 5m to 10m
- dem.py: back up elevation_m as elevation_m_original in timeseries
  before the first DEM overwrite, so original sensor data is preserved
- dem.py: add recalculate_elevation_hysteresis() — recomputes gain/loss
  from original recorded elevation (reads elevation_m_original if a DEM
  run already replaced elevation_m) using source-aware thresholds
  (5m barometric, 10m GPS/unknown); does not touch the elevation array
- edit/server.py, serve/server.py: split /recalculate-elevation into
  two endpoints: /recalculate-elevation/dem and
  /recalculate-elevation/hysteresis
- EditDrawer.svelte: replace single DEM button with two side-by-side
  buttons — "Recalculate (hysteresis)" (fast, offline) and
  "Recalculate (DEM)" (SRTM lookup)
2026-04-20 21:41:23 +02:00
Davide Scaini 1940e2409b feat: DEM-based elevation recalculation via edit drawer button
Adds a "Recalculate from terrain map (DEM)" button to the activity edit
drawer. On click it queries an Open-Elevation-compatible API to replace
GPS altitude with SRTM terrain data, applies 5m hysteresis, and updates
the activity's elevation stats and timeseries chart in place.

- bincio/extract/dem.py: lookup_elevations() (batched HTTP POST) +
  recalculate_elevation() (subsample → DEM → interpolate → hysteresis →
  patch activity JSON, timeseries JSON, index.json)
- POST /api/activity/{id}/recalculate-elevation on both serve and edit
  servers; serve endpoint is auth-gated and triggers merge + rebuild
- --dem-url flag (also DEM_URL env var) on bincio serve and bincio edit;
  logged at startup; missing URL returns a clear 503 with setup instructions
- /api/me response gains dem_configured bool
- EditDrawer: button with loading state, shows new ↑/↓ values on success
2026-04-20 20:45:06 +02:00
Davide Scaini 6491e4fd8c fix: show total activity count in global feed counter
The counter now shows "50 of 16398 activities" using the total from
feed.json, matching the previous behaviour where all activities were
loaded upfront.
2026-04-20 17:12:50 +02:00
Davide Scaini 104328bc50 fix: stable y-axis range and sane dist_km in activity charts
Three bugs in the time↔distance x-axis toggle:

1. GPS speed glitches (e.g. a 1-second spike at 222 km/h) were accumulated
   into dist_km, pushing all subsequent points ~60 m too far right on the
   distance axis and compressing the rest of the chart.  Cap speed at 150 km/h
   during dist_km integration; values above that are treated as 0 movement.

2. Observable Plot auto-infers the y domain from plottable points only.
   When x-mode changes, which points are "plottable" changes too, so the
   y axis range silently shifted between time and distance views.  Fix by
   computing lineDomainMin/Max once from the full dataset and passing an
   explicit domain to Plot.

3. monotone-x curve requires strictly increasing x.  In distance mode,
   stopped segments produce consecutive points with identical dist_km,
   causing NaN Bézier control points and visual artifacts.  Use linear
   curve for distance mode (data is dense enough that it looks smooth).
2026-04-20 17:06:56 +02:00
Davide Scaini db7047f210 perf: combined feed index for multi-user global feed
Instead of the browser resolving 20+ user shards recursively (~27 MB),
generate a pre-sorted feed.json at merge time with 50 activities per
page. The global feed loads one ~30 KB file on first paint; "Load more"
fetches subsequent pages (feed-2.json, feed-3.json, etc.).

Per-user profile pages still use year-sharded loadIndexPaged as before.
2026-04-20 15:31:35 +02:00
Davide Scaini d069716068 fix: clamp stats tooltip within viewport on mobile 2026-04-20 15:07:52 +02:00
Davide Scaini 5227b30456 fix: EditDrawer correctly reads and labels unlisted privacy
- serve/server.py GET adds private:bool to the response (true when
  privacy is "unlisted" or legacy "private") so EditDrawer can read it
- edit/server.py GET: same fix for the single-user edit server
- EditDrawer: fall back to d.privacy if d.private is absent; rename
  "Private" toggle label to "Unlisted"
2026-04-19 22:58:09 +02:00
Davide Scaini 8575a7015b fix: delete activity removes it from index.json; detail page uses lazy load
delete_activity now updates data_dir/index.json so merge_all no longer
re-adds the summary for a deleted activity, preventing the broken
"Activity not found" state after deletion.

ActivityDetailLoader switches from loadIndex (all year shards) to
loadIndexPaged (first year shard only) + direct file fallback, so
opening an activity detail page no longer downloads the entire history.
2026-04-19 22:31:20 +02:00
Davide Scaini cada2bcb03 perf: year-shard index.json to cut initial load from MBs to ~1 year
merge_all/_merged/index.json is now a shard manifest; activities are
split into index-{year}.json files. The feed loads only the most-recent
year on first paint (~200 activities instead of all of them). Older
years are fetched lazily when the user clicks "Load older activities".

Also strips best_efforts / best_climb_m / source from shard files —
these fields are aggregation inputs only, never read by the feed UI.
2026-04-19 22:21:10 +02:00
Davide Scaini a78f6ee3bd fix: strip local image refs with spaces/parens in filenames before markdown render 2026-04-16 10:29:13 +02:00
Davide Scaini cfdd8d2744 fix: image refs in description and broken gallery URLs
- EditDrawer: stop auto-inserting ![filename](...) into description on
  upload — images are tracked via custom.images; the refs only cluttered
  the textarea. Strip any pre-existing refs on load so old sidecars are
  also cleaned up when the drawer is opened.
- ActivityDetail: imageBase now treats detail_url that starts with '/'
  as already-absolute (same fix pattern as track_url / detail_url);
  was prepending ${base}data/ on top of /data/... → double path.
2026-04-16 10:19:32 +02:00
Davide Scaini bfb6432666 fix: force black text in Plot tooltips (white bg, grey text was unreadable) 2026-04-15 22:53:48 +02:00
Davide Scaini 5205a41224 fix: theme-aware chart colors — readable axes and tooltips in light mode 2026-04-15 22:18:06 +02:00