Commit Graph

315 Commits

Author SHA1 Message Date
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
Davide Scaini b1cf18a2f0 fix(feed): update feed.json after every upload so browser sees new activities
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.
2026-04-26 21:37:19 +02:00
Davide Scaini 1c9b89cd1c fix(dev): raise macOS open-file limit before astro dev to prevent EMFILE 2026-04-26 21:16:33 +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 4cabbea0d4 fix(mobile): three-patch compat shim for Chrome <80 (Karoo WebView 61)
Root causes identified via logcat:
  Chrome <71: globalThis not defined → ReferenceError in Pyodide factory,
              loadPyodide stays as {} (set by UMD wrapper), "not a function"
  Chrome <63: dynamic import() is a parse-time SyntaxError
  Chrome <63: for-await-of is a parse-time SyntaxError

Fix: when Chrome <80 is detected, fetch v0.18.1 pyodide.js as text and apply
three split/join patches before injecting via Blob URL:
  1. prepend globalThis polyfill
  2. import( → __loadScript(  (script-tag shim)
  3. for await( → for(        (getFsHandles is never called in our flow)

Chrome 80+ keeps the original clean <script>-tag path with v0.26.4.
2026-04-26 15:19:39 +02:00
Davide Scaini f6885fecfa fix(mobile): use split/join for import() patch, gate behind Chrome<63 check
The regex /\bimport\(/g inside a template literal corrupts \b into a
backspace and \( into (, producing an unterminated-group SyntaxError in the
WebView. Use split().join() instead — no escape sequences, no corruption.

Modern WebViews (Chrome 63+) keep the original clean <script>-tag path.
Old WebViews (Karoo Chrome 61) take the fetch+patch+Blob path.
2026-04-26 15:05:33 +02:00
Davide Scaini 1410be7427 fix(mobile): patch pyodide.js at runtime to bypass Chrome 61 import() syntax
Chrome 61 (Karoo WebView) cannot parse dynamic import() — a SyntaxError at
parse time prevents loadPyodide from ever being defined.

Fix: fetch pyodide.js as text, replace every import( with a __loadScript(
shim that uses <script> tag injection, then inject via Blob URL. The Blob
script is never pre-scanned for module syntax so the patch is invisible to
the parser.

Also: expose waitForEngine() from extractActivity so callers can await
engine readiness before batching files — manual scan now shows "Preparing
extraction engine…" instead of flooding with N individual failures.
2026-04-26 14:59:28 +02:00
Davide Scaini a5c2810568 fix(mobile): gate watch directory on instance URL; skip auto-scan if unconfigured
Settings: watch directory field is hidden behind a warning if no instance
URL is saved yet, making the dependency explicit before the user sets a
path.

Import: runAutoScan silently skips (no errors) when instance URL is
missing; manualScan shows a single clear message instead of one failure
per file.
2026-04-25 22:20:02 +02:00
Davide Scaini 42d829737c fix(mobile): auto-save watch directory on blur, not via Save button
The Save button is anchored to the Instance section near the top. Moving
the auto_import_path write to onBlur on the field itself means the value
is persisted as soon as the user leaves the input — no need to scroll up
and hit Save.
2026-04-25 22:12:59 +02:00
Davide Scaini e062ef5837 fix(mobile): watch-folder button visibility + Karoo file picker crash
- useFocusEffect re-reads auto_import_path from DB every time the Import
  tab is focused, so the scan button appears immediately after saving in
  Settings (was broken by conditional hook call which violated Rules of
  Hooks and never re-fired on setting changes)
- Nested inner try/catch isolates DocumentPicker.getDocumentAsync so
  ActivityNotFoundException (Karoo has no DocumentsUI) shows a friendly
  message instead of crashing the tab
- Settings watch-directory placeholder updated to /sdcard/FitFiles
2026-04-25 22:08:32 +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 749d90c79d fix(android): bundle JS + add armeabi-v7a to ABI splits for Karoo support
Two issues prevented the app from running on the Karoo 2 (armeabi-v7a, Android 8.1):

1. Debug builds loaded the JS bundle from Metro (localhost:8081) rather than
   embedding it. Fixed by setting debuggableVariants = [] in the react {} block,
   which tells the RN Gradle plugin to bundle JS for all variants including debug.

2. splits.abi only included arm64-v8a, so the CMake/NDK build (which generates
   libappmodules.so — the TurboModule registry) never ran for armeabi-v7a.
   Result: PlatformConstants TurboModule not found, app stuck on splash.
   Fixed by adding armeabi-v7a to the splits.abi include list.
2026-04-25 21:44:44 +02:00
Davide Scaini 2f53fbc359 feat(mobile): batch import + Karoo auto-import from watch folder
- Import tab now accepts multiple files at once (DocumentPicker multiple:true),
  processes them sequentially through Pyodide, and shows a summary with per-file
  errors on completion.

- DB migration v2 adds source_path column (original filesystem path before copy)
  and an index on it, enabling O(1) deduplication for watch-folder imports.

- On Android, if auto_import_path is set, the Import tab scans the directory on
  mount and on AppState 'active' (app foreground), then automatically imports any
  FIT files not yet in the DB. Designed for Karoo: finish a ride, open the app,
  new files import without any manual steps.

- insertActivity now accepts optional source_path; both importBasJson and
  importNativeFile pass it through (null for files picked via DocumentPicker,
  real path for watch-folder files).
2026-04-25 21:25:54 +02:00
Davide Scaini a796bf8cae fix: write timeseries directly when wheel's write_activity silently skips it
Diagnosed via on-device debug: build_timeseries produces 3271 points
correctly, but the installed wheel's write_activity has a silent
exception path that skips writing the timeseries file. The workaround
calls build_timeseries directly and writes the file if missing.

Also moves useTheme import to @/ThemeContext across all tab screens.
2026-04-25 20:39:55 +02:00
Davide Scaini 2e520137ac fix: palette changes now propagate immediately via ThemeContext
Replace useSetting()-based useTheme() with a React context (ThemeProvider
+ useTheme/usePaletteControl). The context holds palette key in state so
pressing a palette button in Settings re-renders all screens instantly.
Persists to SQLite and reloads the stored value on mount.
2026-04-25 15:45:27 +02:00
Davide Scaini 91d747c54a feat: seasonal race palette auto-detection on web
Inline script in Base.astro sets --accent / --accent-dim CSS variables
before first paint based on the current date. Switches to pink (Giro),
yellow (Tour), or red (Vuelta) during each Grand Tour window; falls back
to the default blue. Also aligns default --accent with the mobile app
(#60a5fa instead of #00c8ff).
2026-04-25 15:41:45 +02:00
Davide Scaini dfe5307ab4 feat: seasonal race palette (Giro/Tour/Vuelta) + mobile picker
- theme.ts: useTheme() hook with race calendar (May–Sep windows),
  auto-detects Giro/Tour/Vuelta by date; stores override in SQLite
- All screens (feed, import, activity, tab bar) now use accent/dim
  from useTheme() instead of hardcoded #60a5fa
- Settings: Palette section with Auto/Default/Giro/Tour/Vuelta buttons
  to override the auto-detected palette for testing
2026-04-25 15:41:20 +02:00
Davide Scaini 5330b7b489 app icon 2026-04-25 15:33:51 +02:00
Davide Scaini 1ac35c84e0 feat: add delete button for local activities (single and bulk)
- Detail screen: Delete button (top-right, red) with confirmation alert;
  deletes SQLite row and original file via expo-file-system
- Feed screen: long-press card to enter select mode; checkbox + blue
  border on selected cards; bottom action bar with bulk Delete N button;
  header switches to show count + Cancel
- db/queries: deleteActivity (returns original_path) and deleteActivities
  (bulk, returns all original paths)
2026-04-25 13:43:12 +02:00
Davide Scaini c077fceba6 fix: validate activity file exists before treating cached hash as known
The dedup cache (.bincio_cache.json) persists source hashes across runs.
If the activities/ directory is wiped (--fresh in another session, or macOS
clearing /tmp after a restart) while the cache file survives at the user dir
level, re-running extract skips all files as "already extracted" and leaves
activities/ empty. Now only hashes whose corresponding .json file is present
on disk are treated as known, so missing files are always re-extracted.
2026-04-25 09:54:38 +02:00
Davide Scaini e08b024d15 fix: add instanceUrl+token to useEffect deps; add MapLibre Expo plugin
In Hermes release builds, useEffect captures closure values from the first
render. If instanceUrl or token were empty at that moment (before SQLite reads
complete), no fetch ran and map/graphs never loaded. Adding them to the
dependency array ensures the effect re-runs once the values are available;
guards on existing geojson/timeseries state prevent double-fetching.

Also add @maplibre/maplibre-react-native to the Expo plugins array so that
expo prebuild applies the library's required Gradle property configuration to
the Android project.
2026-04-25 09:42:25 +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 69571c1306 fix: pass wheel filename through extraction chain to fix micropip install
micropip requires the full PEP 427 wheel filename (name-version-py-abi-plat.whl)
— writing the file as bincio.whl caused InvalidWheelFilename. The wheel URL from
/api/wheel/version now provides the basename; it flows through fetchWheelBase64 →
extractFile → WebView where the file is written with the correct name and
_wheel_path is set as a Pyodide global before PY_INSTALL_WHEEL runs.
2026-04-25 09:29:33 +02:00
Davide Scaini ef45d4f4bb fix: move PyodideWebView into Import tab; fix micropip blob URL error
Layout fix: WebView as a sibling in the root layout breaks flex geometry even
with position:absolute. Moving it inside the Import tab screen (which Expo Router
keeps mounted after first visit) eliminates the issue entirely and restores the
original simple root layout.

micropip fix: blob: URLs are not a recognised scheme in micropip — they are parsed
as package requirement strings, producing InvalidRequirement. Write the wheel bytes
to Pyodide's Emscripten FS (/tmp/bincio.whl) and install via emfs:/// instead.
2026-04-24 23:26:39 +02:00
Davide Scaini fc814f5026 fix: replace Buffer.from().toString('base64') with btoa helper — Buffer not available in Expo RN 2026-04-24 23:16:50 +02:00
Davide Scaini 21b421fdc3 fix: wrap root layout in flex:1 View so Stack fills full screen alongside hidden WebView 2026-04-24 23:15:52 +02:00
Davide Scaini 84e5cead08 fix: pre-fetch bincio wheel via RN networking to avoid ATS blocking HTTP in WKWebView
WKWebView blocks HTTP requests (ATS) even when NSAllowsLocalNetworking is set for
the app's own networking — so fetch(http://192.168.x.x/api/wheel/download) inside
the WebView always fails with 'Load failed' on iOS.

- extractActivity.ts: rename wheelUrl param to wheelBase64; WebView now receives
  the wheel as pre-fetched base64 bytes rather than a URL to fetch itself
- PyodideWebView.tsx: decode wheelBase64 → Uint8Array → Blob → blob URL for
  micropip.install; fix baseUrl '' → 'https://localhost' (null origin blocks fetch
  on iOS)
- import.tsx: add fetchWheelBase64() that resolves the wheel URL via
  /api/wheel/version then fetches with native networking (HTTP works); caches
  result in memory so repeated imports in one session don't re-download
2026-04-24 23:13:24 +02:00
Davide Scaini 966528a0bf feat: Phase 1 — FIT/GPX/TCX extraction via Pyodide in hidden WebView
- extraction/PyodideWebView.tsx: hidden WebView (1×1 px, off-screen) that
  bootstraps Pyodide v0.26.4 from jsDelivr CDN on app startup; loads lxml,
  pyyaml, micropip, fitdecode, gpxpy automatically; installs the bincio wheel
  lazily on the first extraction call via a blob URL (avoids startup delay)
- extraction/extractActivity.ts: typed bridge — extractFile(filename, base64,
  wheelUrl, onStatus) injects JS into the WebView, tracks pending promises by
  request ID, resolves with { id, detail, timeseries, geojson, sourceHash }
- app/_layout.tsx: mounts <PyodideWebView> outside SQLiteProvider at root so
  the runtime warms up as soon as the app opens
- app/(tabs)/import.tsx: replaces the placeholder alert with real extraction;
  reads files as base64, calls extractFile with a progress callback, stores
  detail_json + timeseries_json + geojson + real SHA-256 source_hash; resolves
  wheel URL via GET /api/wheel/version with fallback to /api/wheel/download;
  falls back to bincio.org if no instance is configured
2026-04-24 22:54:59 +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 c7c7fe9395 feat: bidirectional sync — upload local activities to remote instance
- Server: POST /api/upload/bas accepts pre-extracted BAS JSON (activity + optional timeseries/geojson), writes files and triggers merge_all
- sync.ts: uploadLocalActivities reads unsynced local activities by original_path, POSTs to /api/upload/bas, marks synced_at on success
- Settings: Upload toggle (Off / Upload local activities) in Sync section with subLabel dividers for Download / Upload groups
- Feed: sync message includes uploaded count when activities are pushed
2026-04-24 22:26:13 +02:00
Davide Scaini a1e56e5308 feat: tabbed metric charts — elevation, speed, HR, cadence, power 2026-04-24 22:03:51 +02:00
Davide Scaini 3ce365e439 feat: tap map thumbnail to open full-screen interactive map 2026-04-24 21:55:56 +02:00
Davide Scaini a306682b52 feat: add sync mode setting — summaries only vs full data download 2026-04-24 21:52:25 +02:00
Davide Scaini bb44b80e97 fix: upgrade react-native-svg to 15.15.4 to fix Fabric crash on RN 0.81 2026-04-24 18:20:36 +02:00
Davide Scaini c1e4ab5af7 feat: add reset synced data button to settings 2026-04-24 15:45:51 +02:00
Davide Scaini f054869b04 feat: add --api-host to bincio dev and --mobile to dev_test.py for local mobile testing 2026-04-24 15:43:30 +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 02726034c7 fix: read activity shards in GET /api/feed; improve sync feedback
_merged/index.json is a shard manifest with activities:[] when the user
has >FEED_PAGE_SIZE activities. The endpoint now collects from all
index-{year}.json shard files before returning.

SyncResult gains a `total` field (activities received from server) so the
feed screen can distinguish "server returned nothing" from "all already
stored locally". Messages: "No activities on instance" / "Up to date (N)"
/ "X of N activities synced".
2026-04-24 15:07:52 +02:00
Davide Scaini 44b2878b14 feat: Phase 0.5 — remote feed sync via Bearer token auth
Server (bincio/serve/server.py):
- Add _require_auth: accepts session cookie OR Authorization: Bearer token
- POST /api/auth/token: same as /api/auth/login but returns token in body
  (password used once, not stored; mobile stores only the session token)
- GET /api/feed: auth-gated; reads _merged/index.json for the user and
  returns the activities array as JSON

Mobile:
- db/sync.ts: syncFeed(db) fetches /api/feed, upserts each summary into
  local SQLite as origin='remote'; skips locally-imported activities
- db/queries.ts: add upsertRemoteActivity (INSERT ... ON CONFLICT DO UPDATE
  WHERE origin='remote' — never overwrites local imports); fix feed sort
  order to started_at DESC instead of insertion order
- settings.tsx: Connect section — password field (not persisted) + Connect
  button calls POST /api/auth/token and stores token; Disconnect clears it
- index.tsx: ↓ Sync button + pull-to-refresh both trigger syncFeed; cloud
  badge on remote activities; empty state updated
2026-04-24 12:07:49 +02:00
Davide Scaini 79c572bf8b fix: align expo packages to SDK 54 expected versions
Expo's version checker reported mismatches; update to exact expected versions:
  expo-sqlite ~16.0.10, expo-document-picker ~14.0.8,
  expo-file-system ~19.0.21, expo-background-fetch ~14.0.9,
  expo-task-manager ~14.0.9, react-native-webview 13.15.0,
  typescript ~5.9.2

expo-file-system v19 moved the legacy API (documentDirectory, copyAsync,
makeDirectoryAsync) to 'expo-file-system/legacy' — update import in import.tsx.
2026-04-24 11:45:23 +02:00
Davide Scaini 10b515f3bf fix: pin react-dom to 19.1.0 to prevent npm resolving 19.2.5
expo-router@6.0.23 declares react-dom as peerOptional; without an explicit
pin npm resolves react-dom@19.2.5 (latest) which conflicts with react@19.1.0.
The SDK 54 template pins both to 19.1.0 — mirroring that here.
2026-04-24 11:39:02 +02:00
Davide Scaini 6f5abf634d fix: downgrade to Expo SDK 54 for Expo Go compatibility
Expo Go 54 (latest available on device) cannot run SDK 55 projects.
Rewrite package.json with canonical SDK 54 versions from expo-template-tabs:
  expo ~54.0.33, expo-router ~6.0.23, react 19.1.0, react-native 0.81.5
  expo-sqlite ~15.0.6, expo-document-picker ~13.1.6,
  expo-file-system ~18.0.12, expo-background-fetch/task-manager ~13.1.6,
  expo-notifications ~0.32.16, expo-constants ~18.0.13,
  expo-linking ~8.0.11, expo-splash-screen ~31.0.13,
  expo-status-bar ~3.0.9, react-native-screens ~4.16.0,
  react-native-safe-area-context ~5.6.0
TypeScript compiles cleanly; npm install completes without errors.
2026-04-24 11:32:56 +02:00
Davide Scaini 6e0b619ad3 fix: pin react to 19.2.5 and @types/react to ~19.2.0
react-dom@19.2.5 (pulled by @expo/router-server) requires react@^19.2.5;
@react-native/virtualized-lists@0.85.2 requires @types/react@^19.2.0.
npm install now completes without errors or warnings.
2026-04-24 11:24:16 +02:00
Davide Scaini 18726699f1 fix: update react to 19.2.3 to satisfy react-native 0.85.2 peer dep
react-native@0.85.2 requires react@^19.2.3; package.json had 19.0.0.
Also update @types/react to ~19.1.0 to match.
2026-04-24 11:16:14 +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