Commit Graph

471 Commits

Author SHA1 Message Date
Davide Scaini 7e8545f8db fix: redirect back to activity.bincio.org after login via bincio.org
Pass ?next=<current URL> when bouncing to bincio.org/login/ so the user
lands back on the activity page they came from instead of bincio.org/.
2026-06-03 11:33:37 +02:00
Davide Scaini ae2737fed1 fix: admin page script crashes — define:vars bypasses TypeScript compilation; use data attribute instead 2026-06-03 11:28:38 +02:00
Davide Scaini 4641ca9b72 fix: show Select button only on own profile feed, not community feed 2026-06-03 11:11:47 +02:00
Davide Scaini 7cec9541e2 fix: restore broken <a> tag in nav after Ideas removal 2026-06-03 11:09:26 +02:00
Davide Scaini 37e91af5bd feat: move Ideas into Support page tab; remove Ideas from nav 2026-06-03 11:07:35 +02:00
Davide Scaini 08f451ec71 feat: recurring budget entries (lazy materialise) + preferred Satispay badge 2026-06-03 10:47:58 +02:00
Davide Scaini cf7ce027b1 fix: support page — donate default tab, split community tab, require login 2026-06-03 10:41:59 +02:00
Davide Scaini fa14d91359 feat: Support page with budget transparency (replaces About) 2026-06-03 10:34:18 +02:00
Davide Scaini b781193d44 feat: bulk delete + merge activities in feed
- Select mode in ActivityFeed: toggle with Select button (logged-in only),
  cards become clickable with checkmark indicator, action bar fixed at bottom
- Bulk delete: calls existing DELETE /api/activity/{id} for each selected,
  removes from local feed state immediately
- Bulk merge: POST /api/merge sorts by started_at (earliest = primary),
  sums distance/duration/elevation, weighted-averages HR/power, concatenates
  geojson and timeseries; backs up originals to _merge_backup/ for recovery
- GET /api/merges returns per-user hidden list; feed filters secondaries
  client-side on load so static shards don't need a rebuild to hide them
- POST /api/unmerge/{id} restores primary from backup, unhides secondaries
- ActivityDetail: shows "Merged (N)" badge + Unmerge button for owners
- Fix: edit button now works from personal profile feed (handle was missing
  from year-shard activities; injected from filterHandle on sessionStorage write)
2026-06-03 10:32:02 +02:00
Davide Scaini 5287b98bc1 fix: admin reset-pwd button copies full bincio.org link instead of bare code 2026-06-03 09:42:24 +02:00
Davide Scaini 0e5044eb06 fix: close all bincio-auth migration holes
Pages (register, reset-password, invites) now redirect to bincio.org
like login already did. Admin user-state ops (reset-password-code,
suspend, unsuspend, delete account) are proxied to bincio-auth via
httpx so they write to the correct DB. Adds BINCIO_AUTH_API env var.
2026-06-03 09:36:20 +02:00
Davide Scaini 75f7fa8810 refactoring mobile app location 2026-06-02 23:28:41 +02:00
Davide Scaini 5255e24184 feat: default chart x-axis to distance 2026-06-02 16:49:21 +02:00
Davide Scaini c59fc0073f feat: pace chart and spm cadence for running/hiking/walking/other
Speed tab becomes Pace tab (min/km, y-axis inverted so faster = top).
Cadence label switches to spm. Tooltip and reference lines use m:ss format.
2026-06-02 16:46:16 +02:00
Davide Scaini a142e8732f fix: redirect login to bincio.org (bincio-auth) when PUBLIC_AUTH_URL is set
activity.bincio.org/login/ was issuing plain session tokens; bincio-activity
now validates JWTs, so that path silently broke. Auth wall and logout now
point to the central bincio-auth service instead.
2026-06-02 16:31:46 +02:00
Davide Scaini 13859a34d3 feat: show pace (min/km) for running, hiking, walking, other activities
Cycling keeps km/h; pace sports show e.g. "5:30 /km" in the feed card,
activity stat panel (avg/max), and laps table.
2026-06-02 16:23:03 +02:00
Davide Scaini 1dca00d5e3 update stats script 2026-06-02 15:47:55 +02:00
Davide Scaini fa61801580 refactor: move mobile app to bincio-autarchive repo 2026-06-02 15:45:42 +02:00
Davide Scaini 2af29a460b serve: add JWT consumer shim for bincio-auth integration
When --jwt-secret / BINCIO_AUTH_JWT_SECRET is set, auth is validated
locally by decoding the bincio-auth-issued JWT — no DB session lookup.
Falls back to existing DB-based session lookup when the flag is absent,
so standalone deployments keep working without any config change.

Changes:
- deps.py: add jwt_secret global, _decode_jwt helper, wire into
  _current_user and _require_auth
- cli.py: add --jwt-secret option; log active auth mode on startup
- pyproject.toml: add PyJWT>=2.8 to serve and dev extras
2026-06-02 14:54:43 +02:00
Davide Scaini 0d6bf57932 fix: handle empty/invalid athlete.json in merge, API read, and writer encoding 2026-05-25 20:00:18 +02:00
Davide Scaini 447d56a960 fix: skip empty or unparseable athlete.json in merge_all 2026-05-25 19:55:08 +02:00
Davide Scaini 2f5251e9fe perf: run all background build/merge/rsync subprocesses at nice 19 2026-05-24 19:07:23 +02:00
Davide Scaini c9b544ab55 perf: throttle OG image generation — nice 19 + 50ms sleep between renders 2026-05-24 19:02:08 +02:00
Davide Scaini b827792d16 feat: export gear maintenance log as CSV 2026-05-24 14:07:37 +02:00
Davide Scaini 94cd3f7eb4 fix: show replacement dates as dd/mm/yyyy 2026-05-24 14:01:53 +02:00
Davide Scaini bdee036204 feat: part lifespan tracking in gear tab
API (gear.py):
- POST   /api/gear/{id}/parts
- PATCH  /api/gear/{id}/parts/{pid}
- DELETE /api/gear/{id}/parts/{pid}
- POST   /api/gear/{id}/parts/{pid}/replacements
- DELETE /api/gear/{id}/parts/{pid}/replacements/{rid}

UI (AthleteView.svelte):
- Gear rows are now accordion-expandable
- Collapsed row shows colored status dots (green/yellow/red) per part
- Expanded section: parts list with km-since-replacement colored by threshold,
  Replaced button with date+note form, recent log entries, add-part form
- Contextual suggestion for first part (chain for bikes, shoes for running)
- Edit/delete gear moved into expanded section
2026-05-24 13:40:27 +02:00
Davide Scaini 7db7bf91e0 refactor: extract import_garmin_gear() + add backfill script
Move gear backfill logic from the route handler into
import_garmin_gear(data_dir, user_dir) in garmin_sync.py so it can be
called both from the API and from the CLI script.

scripts/backfill_garmin_gear.py finds all users with Garmin credentials
and runs the backfill for each, printing a per-user summary.
2026-05-24 13:13:47 +02:00
Davide Scaini 801140ac51 feat: show accumulated distance per gear item in gear tab
Compute total distance from allActivities where gear name matches and
display it inline next to each gear item. Also add gear field to
ActivitySummary type so index shard gear data is accessible in the UI.
2026-05-24 13:06:37 +02:00
Davide Scaini 49feef66c5 feat: Garmin gear sync — registry + per-activity gear on sync and backfill
- garmin_sync_iter: sync gear registry from Garmin on every sync run and
  resolve gear for each newly imported activity via get_activity_gear()
- POST /api/garmin/import-gear: one-time backfill that matches Garmin gear
  activities to existing local activities by UTC timestamp (±60 s)
2026-05-24 13:03:34 +02:00
Davide Scaini b23b3de1bb feat: include gear in activity index summaries; generate OG images in serve rebuild 2026-05-24 12:51:57 +02:00
Davide Scaini 5bf426df29 fix: use Strava gear ID prefix (b/g) to determine gear type, not missing primary_type field 2026-05-24 12:44:25 +02:00
Davide Scaini 40ccec0e2d fix: generate OG images in serve rebuild worker, not on every deploy 2026-05-24 12:39:38 +02:00
Davide Scaini e553e08663 feat: gear registry — manage bikes/shoes per athlete, set per activity
- New /api/gear CRUD endpoints (gear.json per user)
- Gear tab in AthleteView (owner-only): add, edit, retire items
- EditDrawer gear field becomes a dropdown when registry has items
- Strava API sync now resolves gear_id → name, adds to registry automatically
- Strava ZIP import reads Gear column from activities.csv
- POST /api/strava/import-gear for one-time backfill from stored originals
2026-05-24 12:33:41 +02:00
Davide Scaini aca9f79b46 fix: slope tooltip clipping — use clip:false + marginTop instead of dy function 2026-05-23 22:24:55 +02:00
Davide Scaini 40aa51be4d fix: flip slope tooltip below dot when near chart top edge 2026-05-23 22:06:02 +02:00
Davide Scaini e5c5383471 fix: move Pillow to base deps so generate_og_images.py can import it 2026-05-23 21:49:47 +02:00
Davide Scaini 693f720cbd feat: OG link previews — track image + meta tags for Telegram/WhatsApp
- bincio/render/ogimage.py: generate 400x400 elevation-coloured PNG with Pillow
- bincio/serve/routers/ogimage.py: /activity/{id}/ OG HTML stub for bot UAs;
  /og-image/{user}/{id}.png serves pre-generated images with on-demand fallback
- scripts/generate_og_images.py: batch pre-generation, incremental (mtime skip)
- scripts/strava_elevation_audit.py: add source/threshold/MA columns and pct stats
- pyproject.toml: add Pillow>=10 to serve extras
2026-05-23 21:44:19 +02:00
Davide Scaini 56932f7f25 perf: add patch_index flag to recalculate_elevation_hysteresis
Allows bulk callers to skip per-activity index.json rewrites and
batch the update themselves, reducing O(n²) index churn to O(n).
2026-05-23 21:05:00 +02:00
Davide Scaini 02edb0b0f9 fix: per-source elevation params — strava_export vs barometric vs raw GPS
Previous thresholds (10 m GPS, 5 m barometric, 30 s MA) were calibrated for
raw noisy GPS. Strava-exported FIT files carry elevation already pre-processed
by Strava (smooth 1 m quantisation, no steps > 5 m), so the aggressive
filtering suppressed real climbing — avg −17 % error across 37 reference
activities.

New strategy, keyed on source + altitude_source:
  strava_export           → MA 5 s, threshold 1.0 m
  fit_file / barometric   → no MA, threshold 1.5 m
  fit_file / gps          → MA 5 s, threshold 2.0 m
  unknown non-strava      → MA 5 s, threshold 1.5 m

Result on 37 cross-referenced activities: avg −2.8 %, std 4.6 %,
37/37 within ±15 % (was 0/37).

Both paths — initial import (metrics._elevation) and bulk recalculate
(dem.recalculate_elevation_hysteresis) — now use the same elevation_params()
function from metrics.py.
2026-05-23 20:12:11 +02:00
Davide Scaini df025873c6 Add map view toggle to activity feed
Adds a List/Map toggle to the feed and @user profile pages. The map view
plots all filtered activities as sport-coloured tracks on a MapLibre map
with no extra requests (uses preview_coords already in memory). Clicking
a track or list row selects it: pans the map to fit, expands the list
item with key stats, and scrolls it into view.
2026-05-22 11:47:47 +02:00
Davide Scaini 7f2a751065 feat: power curve chart on activity page (single-activity MMP) 2026-05-21 21:29:29 +02:00
Davide Scaini 793b719983 fix: stable power curve colors — buttons and chart lines always match 2026-05-21 21:15:44 +02:00
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 418e3a13e8 changelog: document 2026-05-19 performance improvements 2026-05-19 20:19:11 +02:00
Davide Scaini 84eff1f3b0 perf: spatial 10 m downsampling for timeseries
Extract _haversine_m from the inline block in _gps_speed_kmh, add
_spatial_downsample (keep one sample per 10 m traveled, GPS haversine
primary / speed×Δt fallback, indoor activities unchanged), and wire it
into build_timeseries() after the 1 s dedup loop.

Add --downsample-timeseries migration flag to bincio render that applies
the same downsampling to existing stored timeseries files without
re-extracting from original FIT/GPX files.
2026-05-19 20:11:00 +02:00
Davide Scaini 835968e8fe perf: unblock event loop for segment_efforts scan
Extract the synchronous segment-file scan into a plain function and
dispatch it via asyncio.to_thread so it runs in a thread pool instead
of blocking the event loop during concurrent fetches.
2026-05-19 19:53:26 +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 1f6239e7d2 Fix feature detection for explore and add community
explore lives at /u/{handle}/athlete/explore/ — was classified as
profile. Add path-contains check so it's detected correctly.
Add community (/community/) which was falling into the feed catchall.
Extend feature map tuples to (host, startswith, contains, label).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 22:03:40 +02:00
Davide Scaini 5d2e2443a3 Add feature breakdown to stderr output for debugging
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 21:55:14 +02:00
Davide Scaini 90283c45f4 usage_stats: fix reindex alignment bug, switch logins panel to weekly
resample("D") produces midnight-aligned bins; reindexing against a
range built from a raw timestamp (not midnight) caused all values to
be 0. Also switched the logins panel from daily to weekly to match the
feature usage panel — less noisy for a small app.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 21:31:05 +02:00