Commit Graph

171 Commits

Author SHA1 Message Date
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
Davide Scaini dfd56e4448 fix: handle absolute track_url paths in ActivityDetail
resolveShards also rewrites track_url to absolute paths (/data/…).
The trackUrl reactive statement only handled http:// prefixes,
producing double /data//data/… for the GeoJSON fetch → map had no track.
2026-04-15 14:50:10 +02:00
Davide Scaini a75dfa160b F14: add per-activity delete (DELETE /api/activity/{id} + drawer button)
Server endpoint removes the activity JSON, GeoJSON, timeseries, sidecar
edit, and images directory. Also purges the dedup cache entry so the
file can be re-uploaded if needed. Runs merge_all + rebuild afterwards.

EditDrawer: two-click delete button (click once → "Confirm delete?",
click again → deletes). On success, dispatches 'deleted' event.
ActivityDetail navigates back to the feed on delete.
2026-04-13 19:35:40 +02:00
Davide Scaini e6bb6e61a2 fix elevation_gain_m null for modern Garmin FIT files; fix map flash
FIT parser: try enhanced_altitude before altitude. Barometric altimeters
on modern Garmins (Edge 540, 840, etc.) write enhanced_altitude in
record messages and total_ascent in lap messages. The old code read only
altitude, producing null elevation_m per point → null elevation_gain_m
at the activity root while laps had correct values from total_ascent.

ActivityMap: use preview_coords (passed from ActivitySummary) to
initialise the map at the activity's location on mount, eliminating the
flash of world-view before the async detail JSON / bbox arrives.
2026-04-13 19:18:37 +02:00
Davide Scaini 5ad3aee8f6 rename privacy "private" → "unlisted"; enable GPS for unlisted
- "unlisted" = not shown in the public feed, but GPS track, timeseries
  and detail JSON are all accessible by direct URL (security by obscurity)
- "private" accepted as legacy alias everywhere (backward compat with
  existing data on disk)
- New writes from Strava sync / ZIP upload / sidecar use "unlisted"
- Only "no_gps" now suppresses the GPS track
- isUnlisted() helper in format.ts used by all Svelte/Astro components
- SCHEMA.md and CLAUDE.md document the privacy model and the distinction
  between "unlisted" and "no_gps"
2026-04-13 18:49:20 +02:00
Davide Scaini 2ebfc7046d fix: fallback to direct file fetch in ActivityDetailLoader
If the index-based lookup fails (shard fetch silently failed, stale
index state, etc.), try fetching the activity detail file directly from
each user shard's _merged/activities/ directory. This makes private
activities and newly-synced activities accessible even when the index
resolution fails.

Also add console.error logging when shards fail in resolveShards to
help diagnose root causes.
2026-04-13 13:04:46 +02:00
Davide Scaini 78581d5487 redesign community page as sortable table 2026-04-11 14:50:54 +02:00
Davide Scaini 705e00f852 adding community tab 2026-04-11 14:39:19 +02:00
Davide Scaini ff8981b3a1 fix power curve y-axis: use zero:true instead of domain:[0,null] 2026-04-11 10:54:13 +02:00
Davide Scaini 087ef1b776 fix power chart ranges 2026-04-11 09:02:58 +02:00
Davide Scaini cbd5a98cd3 - merge.py: keep private activities in _merged/index.json instead of
stripping them; privacy filtering is now done client-side
      - ActivityFeed: detect logged-in user via bincio:me event; show private
        activities only when viewing your own profile; private cards get a lock
        badge
2026-04-10 23:16:38 +02:00
Davide Scaini c99b755382 The culprit is in renderChart(): it calls chart?.remove() which empties the
container div, causing the layout to collapse to zero height for a moment.
  The browser then scrolls to keep the viewport anchored, but since the page
  got shorter it jumps to the top. When the new SVG is appended, the page is
  taller again but the scroll position was already reset.

  Fix: give the chart container a min-height matching the chart height (220px)
  so it never collapses.
2026-04-10 22:58:34 +02:00
Davide Scaini eeed3fe3b2 Root cause of the 404: _trigger_rebuild was firing bincio render (= full astro build), but:
1. The build took minutes → 404 during that window
  2. Even after the build, the output lands in site/dist/ — nginx serves from /var/www/bincio/ which is only updated by the rsync in the post-receive hook, not by the server process

  Fixes applied:

  1. bincio/render/cli.py: Added --no-build flag — merges sidecars and updates manifests but skips astro build. This is fast (~1 second).
  2. bincio/serve/server.py _trigger_rebuild: Now passes --no-build. After an upload, _merged/ and root index.json are updated immediately, so the feed reflects the new activity. The static Astro pages are
  only rebuilt on git push.
  3. site/src/components/ActivityDetailLoader.svelte (new): Svelte component that reads the activity ID from the URL, calls loadIndex to resolve the shard tree, then renders ActivityDetail dynamically — no
  pre-built page needed.
  4. site/src/pages/activity/index.astro (new): Generic Astro shell page that renders ActivityDetailLoader. Gets compiled to dist/activity/index.html.
  5. docs/deployment/vps.md: Added location /activity/ { try_files $uri $uri/ /activity/index.html; } to the nginx config. When a request arrives for /activity/2026-04-06T153345Z/ and no pre-built file
  exists, nginx serves the shell, which loads the data dynamically from /data/ (which nginx already serves live from disk).
2026-04-10 17:48:23 +02:00
Davide Scaini 61349e6292 fix: resize map before fitBounds and defer with rAF to avoid layout-timing glitch 2026-04-10 17:44:52 +02:00
Davide Scaini ae883a7dba fix: rebuild athlete.json on every ingest; remove bincio-extract references from UI 2026-04-10 15:47:50 +02:00
Davide Scaini f8e02f3da2 fix: athlete page falls back to /api/athlete when static file not yet rebuilt 2026-04-10 15:43:01 +02:00
Davide Scaini f790deb54f fix: athlete page shows empty state instead of error when athlete.json missing 2026-04-10 15:35:41 +02:00
Davide Scaini 084c652fdd fixing stuff after splitting jsons 2026-04-09 15:27:00 +02:00
Davide Scaini 8118f6f316 1 — Timeseries split
- writer.py: timeseries is now written to {id}.timeseries.json as a separate file. The detail JSON gets a timeseries_url field instead. finalize_pending and cleanup_pending handle the extra file.
  - merge.py (merge_one): symlinks the .timeseries.json file alongside the detail JSON. merge_all already handles it transparently (the .timeseries.json stem doesn't match any activity
  ID in to_merge, so it falls through to the symlink branch).
  - types.ts: timeseries is now timeseries?: Timeseries | null, and timeseries_url?: string | null added.
  - dataloader.ts: new loadTimeseries(url, detailUrl, base) function that resolves paths correctly in both single- and multi-user modes (uses the fetched detail URL's directory as the base).
  - ActivityDetail.svelte: loads timeseries separately after detail loads; uses detail.timeseries for IDB activities (embedded) or fetches via detail.timeseries_url for server activities. Charts show a pulse placeholder while loading.

 2 — GZip

  - GZipMiddleware (min 1 KB) added to both bincio/serve/server.py and bincio/edit/server.py — all API JSON responses are now gzip-compressed.
  - For static files (the big timeseries JSONs), nginx should be configured with gzip on; gzip_types application/json application/geo+json; — no code change needed on the server side.

  Net effect: opening an activity page now fetches ~1.4 KB (detail) instead of ~586 KB. The timeseries fetches ~60–150 KB gzip-compressed shortly after (it loads concurrently with the map rendering).
2026-04-09 14:01:02 +02:00
Davide Scaini bf95097d75 fix image 2026-04-09 13:39:44 +02:00
Davide Scaini 5a29259259 fix edit profile 2026-04-09 13:21:47 +02:00
Davide Scaini fb202b4edf Record / Convert tabs — now gated behind PUBLIC_MOBILE_APP=true. Hidden by default in VPS/dev mode; only show when explicitly opted into the mobile app build.
Edit/Upload UI — split into two concepts:
  - PUBLIC_EDIT_URL — the server base URL (empty = use proxy at /api/)
  - PUBLIC_EDIT_ENABLED=true — whether to show the edit/upload buttons at all

  bincio dev now sets PUBLIC_EDIT_ENABLED=true when instance.db exists (multi-user mode), so the upload button, edit button, and edit drawer all appear. The fetch calls already produce  correct relative URLs (${''}/api/upload = /api/upload) which the Vite proxy forwards to bincio serve.
2026-04-09 13:16:00 +02:00
Davide Scaini 50cdeb3b6e ActivityFeed — replaced the <button> inside <a> (invalid HTML, unreliable) with the stretched-link pattern: the card is now a <div>, the title <a> carries a ::before pseudo-element
that covers the whole card making it clickable, and @handle is a proper <a> with z-index: 10 sitting above the stretched link. Clicking the handle navigates to /u/{handle}/; clicking
  anywhere else navigates to the activity.

  ActivityDetail — @handle link added in the date/time row of the header, linking to /u/{handle}/. Only shown when activity.handle is set (i.e. multi-user mode).
2026-04-09 13:02:07 +02:00
Davide Scaini cf7c71b8a3 (opus assessment) Fix auth wall flash, broken multi-user write API, and single-user redirect loop
Auth wall (Base.astro): set data-auth-pending on <body> at SSG time and hide
  it with inline CSS before any JS runs; remove the attribute after /api/me
  resolves. Eliminates the flash of protected content on private instances.

  Multi-user write API (serve/server.py): the previous _apply_sidecar_edit and
  strava_sync imports from bincio.edit.server were broken (those names don't
  exist as module-level exports) and the Strava sync mutated a global data_dir,
  making concurrent requests from different users racy. Fix: extract both
  operations into bincio/edit/ops.py as pure functions that take data_dir
  explicitly. Both edit/server.py and serve/server.py now import from there.

  Security: add rate limiting to POST /api/register (5 attempts / 15 min / IP,
  separate bucket from login). Add _check_id() activity ID validation to both
  GET and POST /api/activity/{id} in serve/server.py.

  Single-user mode: _write_root_manifest now forces instance.private=false when
  no instance.db exists, even if a previous run wrote true. Prevents the auth
  wall from firing and redirecting to /login/ when bincio serve isn't running.

  ActivityFeed: skip filterHandle when profileIndexUrl is set (per-user profile
  pages load the right shard directly; activities have no handle tag at that
  point, so the filter was producing an empty feed). Fix handle links to point
  to /u/{handle}/ instead of /{handle}/. Fix <a>-inside-<a> Svelte warning by
  converting the inner handle link to a <button>.
2026-04-09 09:19:48 +02:00
Davide Scaini 98c42dc443 unify single user and multi user behaviour 2026-04-09 08:59:40 +02:00
Davide Scaini f76cc0ce7e towards multi-user 2026-04-08 19:37:10 +02:00
Davide Scaini 083c67d018 local activity storage and convert page fixes
- Replace rdp dependency with inline pure-Python RDP implementation
    so the bincio wheel runs in Pyodide (no pure-Python wheel existed for rdp)
  - Fix convert page script: remove define:vars so Vite bundles it and
    TypeScript imports (localstore, format) work correctly
  - Rename wheel to proper PEP 427 filename (bincio-0.1.0-py3-none-any.whl)
  - Use en-GB date format on convert result, consistent with the feed
  - Add /activity/local/ page + LocalActivityDetail for IDB-only activities;
    feed links local activities there instead of the SSG route
  - Fix getStaticPaths: try public/data symlink as fallback, never crash on
    missing index.json
  - Fix ActivityDetail.onMount: load detail even when detail_url is absent
    so locally converted activities show map and charts
  - Derive track_url and detail_url from id in toSummary() since they are
    not present in the detail JSON
  - Reload on bfcache restore (pageshow) so client:only components re-mount
    after back navigation
2026-04-08 14:14:42 +02:00
Davide Scaini 5bf0f3636c local conversion 2026-04-06 22:25:57 +02:00
Davide Scaini bd5831c2fd second pass. low 2026-04-01 19:00:28 +02:00
Davide Scaini 3d364c3992 second pass. medium 2026-04-01 11:05:00 +02:00
Davide Scaini 94369606a4 second pass at issues. critical ones. 2026-04-01 10:58:45 +02:00
Davide Scaini 81438231b4 fix low level issues 2026-03-31 23:22:12 +02:00
Davide Scaini 8f91503cf7 fix mid level issues. updated changelog 2026-03-31 23:00:39 +02:00
Davide Scaini f8abab2c23 fix high priority issues 2026-03-31 22:53:50 +02:00
Davide Scaini e2870c3344 fixing issues 2026-03-31 22:40:35 +02:00
Davide Scaini 47568e9dda fix back button behaviour 2026-03-30 22:45:13 +02:00
Davide Scaini 9bc9a1d10f paginate stats page 2026-03-30 21:51:01 +02:00
Davide Scaini cb345c02a1 preserving navigation 2026-03-30 20:27:34 +02:00
Davide Scaini 877472e620 trying to get sub label showed properly 2026-03-30 20:09:01 +02:00
Davide Scaini a65923c3e0 fix unclosed tags 2026-03-30 13:06:31 +02:00