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/).
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.
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.
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.
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.
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.
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
merge_all(user_dir) updates the per-user _merged/ shard but the home page
loads feed.json first via loadCombinedFeed. write_combined_feed was only
called by the CLI render command, not by the API upload endpoints or the
dev watcher, leaving feed.json permanently stale after any runtime upload.
Add write_combined_feed(_get_data_dir()) after every merge_all call in
/api/upload/bas, /api/upload/raw, the dev.py file watcher, and dev startup.
- Skip MapLibre on Android <29 (Karoo): SELinux denies kgsl-3d0 access
from untrusted_app context, crashing the GPU driver on any OpenGL
surface. Replace with SvgRouteView — equirectangular SVG route trace
using react-native-svg, no native GL surface needed.
- Add +/- zoom buttons to full-screen MapLibre map on modern devices
via Camera ref and onRegionDidChange.
- Skip PyodideWebView on Android <29: same GPU driver conflict; set
_engineUnavailable at module init via API level gate (< 29).
- Add engine_unavailable fast path in PyodideWebView: post message
immediately if WebAssembly.Global is absent (Chrome <69) instead of
attempting 30 MB Pyodide download.
- Add server-side extraction fallback (extractServer.ts): when engine
unavailable, POST raw file as base64 to /api/upload/raw; server runs
full Python pipeline and returns extracted data.
- Add /api/upload/raw endpoint in server.py.
- Add pre-flight auth check (checkServerAuth) before batch import so
an expired token errors immediately rather than after N files.
- Fix uploadLocalActivities in sync.ts: was reading original_path as
JSON (binary FIT file, always threw), silently skipping every upload.
Now reads detail_json from DB directly.
- Redesign Feed header: replace single Sync button with Upload /
Download / Refresh. Pull-to-refresh and Refresh button are local-only.
Auto-refresh on tab focus via useFocusEffect.
- Replace ActivityIndicator with plain Text everywhere (native animation
also crashes Karoo GPU driver).
- Raise macOS open-file limit in dev_test.py to prevent EMFILE errors
from Astro file watcher.
- Document all Karoo hardware constraints in docs/mobile-app.md.