42 Commits

Author SHA1 Message Date
Davide Scaini 75f7fa8810 refactoring mobile app location 2026-06-02 23:28:41 +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 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
Davide Scaini cbe3e0eeaf feat(mobile): Karoo GPU crash fix, server-side extraction, upload fix, feed redesign
- 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.
2026-04-26 21:00:12 +02:00
Davide Scaini 44a70f4c18 feat(mobile): watch-folder scan button + Karoo file picker fix + docs
Import screen:
- Add "Scan for new rides" button (green) when auto_import_path is set;
  shows the configured path, lets user trigger manually in addition to
  the automatic scan on app open.
- Detect ActivityNotFoundException from DocumentPicker (Karoo and other
  stripped Android devices have no DocumentsUI app) and show a friendly
  message directing users to set a Watch directory instead.
- "No new rides found" feedback when manual scan finds nothing.

Docs (docs/mobile-app.md):
- Phase 1 marked complete with implementation notes (wheel delivery,
  timeseries workaround, source_path dedup).
- Phase 2 updated to reflect what is actually implemented (on-open scan,
  not background task) vs what remains (true background polling).
- Batch import moved from Phase 5 todo to done.
- Data model updated with source_path column.
- Known gaps section revised to remove fixed stubs.
- New Karoo sideloading section with debuggableVariants and armeabi-v7a
  troubleshooting notes.
2026-04-25 21:52:03 +02:00
Davide Scaini d8b3a69564 fix: allow HTTP to local instances in release build; fix activity 404 in dev
Android release builds block cleartext HTTP by default (debug builds override
this via the debug manifest overlay). Add usesCleartextTraffic=true to app.json
so expo prebuild includes it in the generated manifest — required to reach local
Bincio instances over HTTP.

In bincio dev (Astro dev server), /activity/<id>/ routes 404 because
getStaticPaths() returns [] and there is no nginx try_files fallback. Add a Vite
middleware plugin to astro.config.mjs that rewrites /activity/<id>/ to /activity/
in dev, matching what nginx does in production.
2026-04-25 09:30:02 +02:00
Davide Scaini ed738ffc97 docs: document current mobile app state — gaps, stubs, and missing plan items
- Mark Phase 1 as the critical unbuilt feature; note its prerequisite chain
  (SHA-256 dedup, re-extract button, Phase 2 auto-import all depend on it)
- Flag Phase 2 auto_import_path as stubbed in UI but unimplemented in background
- Add "Known gaps and technical debt" section covering:
  - source_hash stub (id+length, not SHA-256)
  - FIT/GPX/TCX import placeholder alert
  - auto_import_path field with no backend task
  - feed pagination (getAllSync with no LIMIT)
  - individual activity deletion (missing from plan)
  - feed search and filter (missing from plan)
  - token expiry / inline reconnect flow (missing from plan)
  - app icon and splash screen (Expo defaults)
  - upload skip behaviour for origin=remote rows
- Add Phase 5 items not previously in the plan: app icon, feed search/filter,
  individual deletion, token reconnect prompt
2026-04-24 22:42:20 +02:00
Davide Scaini 97c7fae9be feat: Phase 4 — MapLibre route map + SVG elevation chart on activity screen
- Add /api/activity/{id}/geojson and /api/activity/{id}/timeseries endpoints
  (bearer-token-gated, falls back from _merged to raw activities dir)
- Rewrite activity detail screen with MapLibreGL v11 API (Map, Camera,
  GeoJSONSource, Layer) and react-native-svg area chart with gradient fill
- On-demand fetch for remote activities that have no local geojson/timeseries
- Add react-native-svg dependency; requires dev build (npx expo run:android)
2026-04-24 15:40:10 +02:00
Davide Scaini 02bb8a3dd7 feat: serve bincio wheel locally for mobile dev testing
- Add GET /api/wheel/download to serve/server.py and edit/server.py:
  serves dist/bincio-*.whl via FileResponse; in production nginx takes
  the request before FastAPI, so this is a no-op there but works locally
- wheel_version response now includes api_url: "/api/wheel/download"
  alongside the nginx-served url field
- Bundle mobile/assets/bincio.whl (built from dist/) as an offline
  fallback for Pyodide testing before the first instance sync
- docs/mobile-app.md: document dev setup — bundled asset, local server
  endpoint, and how to refresh the bundle with uv build + cp
2026-04-24 11:01:24 +02:00
Davide Scaini b37df88fe1 feat: Phase 0 mobile app scaffold — Expo 55, SQLite, Feed/Import/Settings screens 2026-04-24 10:39:06 +02:00
Davide Scaini 565f5a3ff1 docs: complete mobile app plan — phased roadmap, Android/iOS divergences, data model 2026-04-24 10:26:58 +02:00
Davide Scaini 61479fe554 docs: mobile app — Pyodide/hidden-WebView extraction model, algorithm-travels-to-data pattern 2026-04-24 10:18:49 +02:00
Davide Scaini e952d9bdc1 docs: expand mobile app design — hybrid extraction, Karoo integration, platform independence vision 2026-04-24 10:12:36 +02:00
Davide Scaini 81ed5e1b0b docs: add mobile app design document (local-first, Expo/React Native) 2026-04-24 10:04:13 +02:00
Davide Scaini 0f1876a33c chore: untrack CLAUDE.md, publish.sh, docs/squash-for-github.md; gitignore dns/nginx scratch files 2026-04-22 17:22:31 +02:00
Davide Scaini 7c171c9e9d docs: replace VPS IP with placeholder in elevation.md 2026-04-22 17:14:10 +02:00
Davide Scaini 88b24a6274 docs: update elevation docs and changelog for two-button recalculation and DEM fix 2026-04-20 21:43:28 +02:00
Davide Scaini 2b7a37ed41 docs: update changelog, CLI reference, user guide, and elevation notes
- CHANGELOG: document hysteresis elevation fix and DEM recalculation feature
- docs/reference/cli.md: add --dem-url to bincio edit and bincio serve tables
- docs/user-guide.md: document "Recalculate from terrain map" button in edit drawer
- docs/elevation.md: mark both short-term and medium-term fixes as implemented
2026-04-20 21:18:50 +02:00
Davide Scaini e8a5fbbaba docs: add nginx gzip configuration for JSON compression
Activity index shards compress ~90% with gzip (130 KB → 14 KB).
The default nginx.conf has gzip on but gzip_types commented out,
so JSON was served uncompressed.
2026-04-20 15:24:16 +02:00
Davide Scaini cea1dbc2fb ops: fix data/ triple-duplication costing ~24 GB on VPS
astro build resolves the public/data symlink and copies all activity JSON
into dist/; rsync then copied that to the webroot — but nginx already serves
/data/ directly from /var/bincio/data/ via alias, so both copies were dead
weight. Freed 36 GB → 14 GB on the live server.

- post-receive hook: prune dist/data/ before rsync, add --exclude=data/
- docs: update manual rebuild command and nginx comment to match
- serve/server.py: _mb() now uses lstat() to count symlinks at face value
  rather than following them to targets, so admin storage panel no longer
  double-counts _merged/ (which is mostly symlinks into activities/)
2026-04-19 23:34:55 +02:00
Davide Scaini c68dfa9057 chore: update changelog, remove stale files, scrub VPS IP
- CHANGELOG.md: add [Unreleased] 2026-04-16 section covering settings
  page, admin tools, password reset, re-extract, community page, SSE
  upload progress, and all bug fixes since 2026-04-10
- Remove docs-proposal.md (internal planning doc, not user-facing)
- Remove publish/ directory (leftover artefacts from publish.sh, not
  meant to be tracked)
- scripts/pull_feedback.sh: replace hardcoded default VPS IP with a
  required positional argument to avoid leaking server address
- docs/squash-for-github.md: document the squash-for-github commit
  strategy for future reference
2026-04-16 18:09:32 +02:00
Davide Scaini 395182649b improve docs 2026-04-15 23:07:52 +02:00
Davide Scaini 6890892654 trying to fix building of activities that fails because of OOM 2026-04-15 09:30:22 +02:00
Davide Scaini fcc70a8d90 fix graph.html: set explicit pixel height for vis.js container
vis.js requires a pixel-sized container — flex:1 is ignored.
Use position:fixed toolbar + JS-measured height for the graph div,
stored as window._network for resize handling.
2026-04-14 22:48:37 +02:00
Davide Scaini a14cee8710 add architecture graph generator and docs
scripts/gen_graph.py parses FastAPI routes, frontend fetch() calls,
component imports, and Python imports to auto-generate:
- docs/architecture.mmd: Mermaid diagram with API/Pages/Components/Python subgraphs
- docs/graph.html: standalone vis.js interactive graph (dark theme, group filters,
  search highlight, click-to-highlight connected nodes)

docs-proposal.md: proposal for a docs/ folder structure, API documentation strategy,
and tooling recommendations (plain markdown → MkDocs Material).
2026-04-14 22:45:03 +02:00
Davide Scaini 6d702ed454 modify post hook to install garmin packages 2026-04-12 15:47:09 +02:00
Davide Scaini f003fdd89f garmin sync first attempt 2026-04-12 15:36:21 +02:00
Davide Scaini 6c431e8821 Here's what was built and why each decision was made:
Key at data_dir.parent/.garmin_key — nginx serves location /data/ { alias /var/bincio/data/; } so
  anything inside that dir is reachable. The key lives one level up at /var/bincio/.garmin_key,
  outside nginx's reach.

  Two-layer storage — garmin_creds.json holds the encrypted email+password (needed for re-login when
  tokens expire); garmin_session/ holds the garth OAuth tokens in plain JSON (short-lived, not the
  user's actual password).

  test_login() — called by the connect endpoint before saving anything, so credentials are only
  persisted if they actually work.

  get_client() — tries the session first (fast, no network), falls back to full re-login
  transparently. The caller never needs to think about whether the session is fresh.
2026-04-12 15:12:20 +02:00
Davide Scaini 0b569b727c trigger site rebuild after new user registration so profile pages exist immediately 2026-04-11 14:19:40 +02:00
Davide Scaini ef5b06c5b3 trigger rebuild after activities upload 2026-04-11 08:47:27 +02:00
Davide Scaini 3b8bc159c5 upload strava zip 2026-04-10 22:01:44 +02:00
Davide Scaini 3e4ff4019b limit number of workers 2026-04-10 18:13:49 +02:00
Davide Scaini e2765ea012 fix for strava auth 2026-04-10 18:01:32 +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 96a3deee5d fix: serve data/ from disk via nginx alias; return full athlete data from API 2026-04-10 15:57:23 +02:00
Davide Scaini 5371c77c8f update vps instructions 2026-04-10 15:14:44 +02:00
Davide Scaini ceb8e28b74 update deployment instructions 2026-04-10 15:06:31 +02:00
Davide Scaini 4593478863 feedback page 2026-04-10 14:23:31 +02:00
Davide Scaini 5170afa9e8 vps instructions 2026-04-10 12:53:35 +02:00
Davide Scaini 98c42dc443 unify single user and multi user behaviour 2026-04-09 08:59:40 +02:00
Davide Scaini 2007f53580 reorg documentation 2026-04-08 19:37:33 +02:00