Commit Graph

363 Commits

Author SHA1 Message Date
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 4d2df860ce Segments Phase 3: detection algorithm, CLI, ingest hook, and efforts API
- detect.py: ActivityTrack + detect_one/detect_all (bbox pre-filter →
  start/end proximity 25m → path conformance 50m/30% → effort extraction
  with avg speed/HR/power and Coggan NP)
- cli.py: `bincio segments detect` for retroactive detection over stored
  timeseries JSONs, with optional --activity-id / --segment-id filters
- ingest.py: non-fatal hook at end of ingest_parsed runs detect_all
- server.py: GET /api/segments/{id}/efforts and POST /api/segments/{id}/detect
2026-05-13 00:50: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 79cad29ff1 Segments phase 1: models, store, and API endpoints (GET/POST/DELETE /api/segments) 2026-05-13 00:19:15 +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 0b266d208c Strip pre-2000 leading points to prevent epoch-zero start time and absurd duration 2026-05-12 23:11:33 +02:00
Davide Scaini 867da767eb Add sub_sport editing to activity edit drawer 2026-05-12 23:01:12 +02:00
Davide Scaini 93f6109028 Add hamburger menu for mobile nav 2026-05-11 11:37:33 +02:00
Davide Scaini 8fbbf460a9 Add PWA icons and manifest for iOS/Android home screen 2026-05-11 11:17:13 +02:00
Davide Scaini 14313ec59c Add Disconnect button to Strava section of upload modal 2026-05-10 17:12:55 +02:00
Davide Scaini 1eaf5c4e0b Remove TS annotation from define:vars script (caused parse error) 2026-05-10 17:10:38 +02:00
Davide Scaini 5be58f4e1c Fix Strava OAuth popup detection via postMessage (cross-origin safe) 2026-05-10 17:04:30 +02:00
Davide Scaini 695dc9fdce Fix Strava re-auth when credentials change; add disconnect button
When a user saves new Strava credentials with a different client_id,
auto-delete the existing token (it belongs to a different OAuth app
and will always fail on refresh). Add POST /api/strava/disconnect
endpoint and a "Disconnect from Strava" button in settings, visible
only when connected.

Immediate: deleted diego_p's stale token so he can reconnect.
2026-05-10 16:33:52 +02:00
Davide Scaini 8f028101c7 Fix elevation gain inflation from device no-fix leading zeros
Apple Watch and similar devices record exactly 0.0 for elevation while
waiting for barometric/GPS lock, then jump to the real altitude. The
hysteresis accumulator was seeding from 0.0, counting the full jump as
ascent. Fix: detect a leading near-zero run followed by a large jump
and seed the accumulator from the first real value instead.

Applied in both _elevation() (fresh extractions) and
recalculate_elevation_hysteresis() (recompute path). Added a bulk
admin endpoint POST /api/admin/users/{handle}/recompute-elevation and
corresponding button to fix existing stored activities.
2026-05-10 16:21:24 +02:00
Davide Scaini 55d59112ad Fix: don't copy 9 GB data dir into dist/ during production builds
BINCIO_DATA_DIR is already set in the build env, so manifest.ts reads
the data root directly at build time without needing public/data.
Moving _link_data() into the serve-only branch prevents Astro from
following the symlink and copying the full data dir into dist/. Any
leftover symlink from a previous dev session is removed before build.
Dev mode is unchanged.
2026-05-08 13:56:31 +02:00
Davide Scaini 2287d6e2ee Add Strava sync status report and manual trigger to admin panel
Each sync run now writes _strava_sync_status.json per user (status,
imported count, error message). New admin endpoints expose this data
and allow triggering an on-demand sync. The admin page gains a Strava
Sync section showing per-user token/credentials state, total imported,
last sync time, and last-run status with inline error messages.
2026-05-08 13:44:23 +02:00
Davide Scaini 12693dbd60 feat: scheduled Strava sync + admin suspend/delete account
- Add bincio sync-strava command: headless multi-user Strava sync
  designed for systemd timer. Discovers users via strava_token.json,
  skips users without their own strava_credentials.json, respects
  Strava visibility (only_me → unlisted). Treats 404 stream errors as
  no-GPS activities rather than retrying every run.
- Add deploy/systemd/bincio-sync.{service,timer}: runs every 3 hours,
  Persistent=true to catch up after downtime.
- Add POST /api/internal/rebuild: webhook for sync timer to trigger
  site rebuild, authenticated via X-Sync-Secret header.
- Add suspended column to users table with auto-migration on open_db.
  Suspended users are blocked at login and session lookup (covers both
  activity site and wiki, which share instance.db).
- Add POST /api/admin/users/{handle}/suspend|unsuspend and
  DELETE /api/admin/users/{handle}/account endpoints.
- Admin panel: Suspend/Unsuspend toggle, Del account button, suspended
  badge on user row.
2026-05-08 10:36:21 +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 48ffc5be8e Hub: add SW cleanup on load; add self-unregistering sw.js to kill stale Astro SW 2026-05-03 18:47:30 +02:00
Davide Scaini 8c10ff5574 fix card hub not being showed 2026-05-02 23:10:28 +02:00
Davide Scaini 9540cdd6cb Replace Astro hub build with standalone hub/index.html
CI / Python tests (push) Waiting to run
CI / Frontend build (push) Waiting to run
2026-05-02 22:49:32 +02:00
Davide Scaini 58def4bf02 weird characters 2026-05-01 23:49:48 +02:00
Davide Scaini e0b5a55ccc fix(dev): pass --host 0.0.0.0 to astro dev when --mobile, print LAN URL 2026-05-01 22:20:19 +02:00
Davide Scaini 12ef5100ef fix(hub): greeting above tagline, no punctuation 2026-05-01 22:08:27 +02:00
Davide Scaini 0ab62aa961 fix(hub): activity card links to /u/{handle}/ when same origin to avoid hub loop 2026-05-01 22:07:01 +02:00
Davide Scaini 1f11bee730 feat: bincio.org hub page (login / app selector) and grants_activity invite toggle 2026-05-01 22:04:30 +02:00
Davide Scaini 82288a35ea feat(auth): wiki/activity access flags, SESSION_DOMAIN, wiki nav link 2026-05-01 21:56:02 +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 5e36806392 feat(mobile): replace 'This year' with dynamic per-year pills in filter tab
Date row now shows All time | 7d | 30d | 6mo | 2026 | 2025 | ... derived
from actual activity data. Year pills use a bounded [Jan 1, Jan 1+1) range
via a new dateTo field on ActivityFilter; rolling-window presets keep an
open upper bound.
2026-04-27 15:37:09 +02:00
Davide Scaini 87baf33815 fix collapsing labels 2026-04-27 15:28:39 +02:00
Davide Scaini 946da685e5 feat(mobile): editable activity title for local activities
Adds edits_json column (migration v3) to store user overrides separately
from detail_json so Option A server re-extraction never clobbers them.

- Tap the title in the detail screen to edit (local activities only, shown
  with a ✎ hint). Saves on keyboard dismiss via onEndEditing.
- Cards and search display user_title ?? title.
- Raw upload: user_title sent to server -> sidecar written so web UI shows
  the correct title (server re-extracts from FIT, which has Karoo's title).
- BAS upload: detail.title overridden before sending, no sidecar needed.
2026-04-27 15:20:19 +02:00
Davide Scaini 090d4bd8dc feat(mobile): search/filter tab — sport, date, sort; hidden on Karoo
Adds a fourth tab visible only on Android API 29+ (full phone, not Karoo).
Filters by sport pill, date preset (7d/30d/6mo/year), and sort order
(newest/distance/elevation). Paginated FlatList with the same activity cards.

ActivityCard extracted to mobile/components/ActivityCard.tsx so both the
feed tab and the new search tab share the same component without duplication.
2026-04-27 15:10:35 +02:00
Davide Scaini be772bd3df fix(upload): prevent false 422s and EMFILE crash during bulk uploads
Four related issues made uploading 271+ activities unreliable:

1. merge_all/write_combined_feed were inside the extraction try/except —
   any merge race returned 422 even though the file was on disk, causing
   the mobile app to permanently mark the upload as failed.  Fixed by
   moving them to a separate best-effort try/except after the extraction
   block.  Switch to merge_one (single-activity symlink) instead of
   merge_all (full rebuild) so each upload is O(1) FS ops, not O(N).

2. The dev watcher fired merge_all for every activity .json write AND the
   upload endpoint also ran merge_all — O(N²) symlink operations during
   bulk uploads.  Watcher now skips activities/*.json changes (upload
   endpoint handles those directly).

3. Vite/Chokidar followed the public/data symlink and opened a handle per
   activity file; constant merge rebuilds exhausted file descriptors and
   crashed the Astro dev server.  Fixed with watch.ignored on public/data.

4. _write_year_shards and write_combined_feed used f.unlink() without
   missing_ok=True — concurrent callers racing the same file threw
   FileNotFoundError which propagated as a false extraction failure.
2026-04-27 14:33:05 +02:00
Davide Scaini 7a65ed2078 fix(mobile): clear technical debt — real SHA-256, feed pagination, search
source_hash: BAS JSON import now computes SHA-256 via crypto.subtle.digest
instead of the '${id}-${length}' stub. No extra package — Hermes supports
Web Crypto API natively.

Feed pagination: useActivities(query, limit) accepts a LIMIT parameter.
The feed screen starts at 50, calls loadMore() via FlatList onEndReached
(threshold 0.3) to increment by 50 each time. useActivityCount(query)
drives the hasMore guard so loadMore is a no-op at the end of the list.

Feed search: compact TextInput below the header filters by title via
SQLite json_extract LIKE. Changing the query resets limit to PAGE_SIZE
so stale paginated results don't linger.

Docs: close the three resolved debt items; keep only the accepted
background-polling limitation as a known gap.
2026-04-27 11:53:43 +02:00
Davide Scaini 93247d510f feat(mobile/upload): upload_format setting + Option A local update from server response
Settings → Sync → Upload format: "Original file" (default) / "Extracted JSON".
- raw (default): reads original_path as base64, POSTs to /api/upload/raw; after
  success, overwrites local detail_json/timeseries_json/geojson/source_hash with
  the server's DEM-corrected extraction (Option A). Falls back to bas if the file
  is missing.
- bas: POSTs pre-extracted JSON to /api/upload/bas, faster, no DEM correction.

Switching modes is safe — the server deduplicates by activity id so a previous
raw upload will return status:"duplicate" on a subsequent bas attempt.
2026-04-27 11:44:32 +02:00
Davide Scaini 0d2176aef0 feat(mobile/upload): send original FIT file via /api/upload/raw when available
When original_path is set (i.e. the original FIT/GPX/TCX is still on disk),
upload via POST /api/upload/raw { filename, base64 } so the server re-extracts
with DEM elevation correction.  Falls back to /api/upload/bas (pre-extracted
BAS JSON) when original_path is null or the file has been deleted.
2026-04-27 11:18:29 +02:00
Davide Scaini cfb3ba5871 fix(merge): serialize merge_all per user-dir to prevent concurrent rmtree race
When the dev file-watcher and an upload endpoint both trigger merge_all on the
same user directory at the same time, the shutil.rmtree/_merged/activities/
sequence collides: one thread deletes the directory while the other is mid-walk,
causing ENOTEMPTY or ENOENT crashes.

Added a per-user-dir threading.Lock (_merge_lock) at the top of merge.py so
all callers (upload endpoints, watcher, admin rebuild) are serialized without
changing any call site.  Also made the rmtree ignore_errors=True and the
symlink_to conditional to survive any stale leftover from a prior crash.
2026-04-27 11:11:05 +02:00
Davide Scaini 220efb0d05 fix(mobile/upload): activities now appear in browser after upload; reconcile synced_at on fresh server
Three bugs fixed:
- /api/upload/bas and /api/upload/raw never updated user_dir/index.json, so
  merge_all couldn't include uploaded activities in year shards — they existed
  on disk but were invisible to the browser feed. Fixed by _upsert_index_summary()
  called before merge_all().
- Silent catch {} in uploadLocalActivities swallowed all per-activity errors;
  replaced with console.warn so failures are visible in Expo logs.
- After a server wipe, synced_at flags on the device caused "Nothing to upload"
  forever. uploadFeed() now reconciles against GET /api/feed at the start of each
  upload: local activities not found on the server get synced_at cleared.

Also: live upload progress ("Uploading N / M…"), failed count in result message,
onProgress callback on uploadFeed(), countPendingUploads() helper.
2026-04-27 11:03:00 +02:00