remove accidentally tracked local files; update .gitignore
This commit is contained in:
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Read(//Users/brutsalvadi/src/bincio_data/_merged/**)",
|
|
||||||
"Read(//Users/brutsalvadi/src/bincio_data/**)",
|
|
||||||
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(list\\(d.get\\(''''dependencies'''',{}\\).keys\\(\\)\\)\\)\")",
|
|
||||||
"Bash(uv run:*)",
|
|
||||||
"Bash(python3 -c \"import pyodide\")",
|
|
||||||
"Bash(uv build:*)",
|
|
||||||
"Bash(npm install:*)",
|
|
||||||
"Bash(npm run:*)",
|
|
||||||
"Bash(mkdir -p /tmp/bincio_ci/activities)",
|
|
||||||
"Bash(BINCIO_DATA_DIR=/tmp/bincio_ci npm run build)",
|
|
||||||
"Bash(ls /Users/brutsalvadi/src/bincio_activity/*.md)",
|
|
||||||
"Bash(python3:*)",
|
|
||||||
"Bash(wc -l /Users/brutsalvadi/src/bincio_activity/bincio/extract/*.py)",
|
|
||||||
"Bash(grep -E \"\\\\.py$\")",
|
|
||||||
"Bash([ -f ~/src/cycling_data_davide/activities.csv ])",
|
|
||||||
"Read(//Users/brutsalvadi/src/cycling_data_davide/**)",
|
|
||||||
"Bash(ls -la /tmp/bincio_dev_test/dave/_merged/activities/*.json)",
|
|
||||||
"Bash(ls -la /tmp/bincio_dev_test/dave/_merged/activities/*.geojson)",
|
|
||||||
"Bash(unzip -l ~/src/cycling_data_davide/export_18885842.zip)",
|
|
||||||
"Bash(unzip -p export_18885842.zip profile.csv)",
|
|
||||||
"Bash(unzip -p export_18885842.zip bikes.csv)",
|
|
||||||
"Bash(unzip -p export_18885842.zip routes.csv)",
|
|
||||||
"Bash(unzip -p export_18885842.zip privacy_zones.csv)",
|
|
||||||
"Bash(unzip -p export_18885842.zip activities.csv)",
|
|
||||||
"Bash(xargs -I{} python3 -c \"import json,sys; d=json.load\\(open\\('{}'\\)\\);print\\({k:d.get\\(k\\) for k in ['title','strava_id','started_at','privacy']}\\)\")"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,6 +29,14 @@ bincio_data/
|
|||||||
.env
|
.env
|
||||||
extract_config.yaml
|
extract_config.yaml
|
||||||
|
|
||||||
|
# Local working / scratch files
|
||||||
|
advice.md
|
||||||
|
issues.md
|
||||||
|
todo.md
|
||||||
|
site/public/data
|
||||||
|
site/public/*.whl
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
# Capacitor native projects
|
# Capacitor native projects
|
||||||
# Commit these if you want to track native code changes;
|
# Commit these if you want to track native code changes;
|
||||||
# omit these lines if you regenerate them from `npx cap add`
|
# omit these lines if you regenerate them from `npx cap add`
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
# Advice on ARCHITECTURE.md
|
|
||||||
|
|
||||||
Review of the mobile / offline plan in `ARCHITECTURE.md`. The two-stage extract/render pipeline, edit flow, and federation sections are accurate and clear — this document focuses on where the architectural risk lives: the mobile app and the path to fully-offline operation.
|
|
||||||
|
|
||||||
## What's strong
|
|
||||||
|
|
||||||
- **Workflow framing (1–4) is the right way to think about it.** Each workflow is a concrete user story, not an abstract capability list. Workflow 1 (record → convert → cloud) is achievable with what's already there; that's a good wedge to ship first.
|
|
||||||
- **Honest status table** at the bottom of "Fully offline — missing pieces". An architecture doc that admits what isn't done is more useful than one that hand-waves.
|
|
||||||
- **Incremental validation plan (§5).** Testing the service worker approach in the browser before touching native builds is the right call — cheap to run, and the failure mode is informative rather than expensive.
|
|
||||||
|
|
||||||
## Decision: drop `capacitor-nodejs`
|
|
||||||
|
|
||||||
The doc previously listed `capacitor-nodejs` as a fallback if service workers fail on iOS. We are dropping this option entirely. Reasons:
|
|
||||||
|
|
||||||
- It embeds a full Node runtime, meaningfully increasing app size.
|
|
||||||
- iOS support relies on a fork of Node-mobile and has historically been spotty.
|
|
||||||
- It introduces a long-term maintenance burden (tracking upstream Node, plugin updates, two runtimes to debug).
|
|
||||||
- The "Hard" effort label in the original status table understated both the integration risk and the ongoing cost.
|
|
||||||
|
|
||||||
If service workers turn out to be blocked on iOS, the fallback should be one of:
|
|
||||||
|
|
||||||
1. **A data access abstraction** (see next section) that lets the app read from IndexedDB directly via a JS loader, with no `fetch('/data/*')` in the hot path at all. This sidesteps the WKWebView question entirely.
|
|
||||||
2. **A native Swift/Kotlin micro-server** if a local HTTP origin is genuinely required.
|
|
||||||
3. **Bundling data as static assets** and re-running `cap sync` on import — crude but boring and reliable.
|
|
||||||
|
|
||||||
`capacitor-nodejs` should not appear in the doc, the test plan, or the status table.
|
|
||||||
|
|
||||||
## Things to fix or add
|
|
||||||
|
|
||||||
### 1. Add a data access abstraction step *before* the service worker work
|
|
||||||
|
|
||||||
The doc frames the offline problem as "serve local data to the WebView at `/data/*`". That skips a prior question: does the Astro site actually need to fetch `/data/*` at runtime, or can the data layer be abstracted behind a thin loader (`loadIndex()`, `loadActivity(id)`) with two implementations?
|
|
||||||
|
|
||||||
- **Cloud build:** loader uses `fetch('/data/...')` as today.
|
|
||||||
- **App build:** loader reads from IndexedDB directly.
|
|
||||||
|
|
||||||
This is a much smaller change than service worker interception, avoids the iOS WKWebView question entirely, and is useful even if you stay cloud-only (testability, mocking, future federation transports). It should land *before* any service worker work — at which point the SW work may turn out to be unnecessary.
|
|
||||||
|
|
||||||
### 2. Commit to JS for sidecar merge — do not use Pyodide
|
|
||||||
|
|
||||||
The "missing pieces" section lists two options for re-running `merge_all` on save: reimplement in JS (~150 lines) or call into Pyodide. Pick JS. Reasons:
|
|
||||||
|
|
||||||
- Pyodide is lazy-loaded by `/convert/`. It is **not** warm just because the app is open.
|
|
||||||
- A user tapping Edit → change title → Save would trigger a multi-second Pyodide cold start (~10MB) for a one-line edit. Terrible UX, and it repeats on every cold app launch.
|
|
||||||
- Porting `merge_all` to JS keeps the edit drawer decoupled from the convert page's machinery. The two subsystems stay independent.
|
|
||||||
|
|
||||||
Pyodide should remain convert-page-only.
|
|
||||||
|
|
||||||
### 3. The Workflow 4 diagram contradicts the test plan
|
|
||||||
|
|
||||||
The "Fully offline on phone" workflow shows a "Local Node server" arrow as if it were the chosen path. The §5 test plan picks service workers first. The diagram should reflect that — show the SW path as primary, and drop the Node server box entirely (per the decision above).
|
|
||||||
|
|
||||||
### 4. Missing: data lifecycle on device
|
|
||||||
|
|
||||||
Nothing in the doc covers:
|
|
||||||
|
|
||||||
- How much storage the app is allowed to use on iOS before the OS evicts it.
|
|
||||||
- What happens on uninstall / reinstall.
|
|
||||||
- Sync / conflict resolution if the same activity exists locally and on a cloud instance.
|
|
||||||
|
|
||||||
These don't need solutions today, but they should be acknowledged as open questions. Otherwise Workflow 4 will hit all of them at once during implementation.
|
|
||||||
|
|
||||||
### 5. Reconcile Pyodide payload size
|
|
||||||
|
|
||||||
Line 162 says "~8MB", line 335 says "~10MB". Pick one and use it consistently.
|
|
||||||
|
|
||||||
## Small stuff
|
|
||||||
|
|
||||||
- The federation diagram uses a `Note1` node that won't render as a Mermaid note — it'll appear as a regular box. Use `%%` comments or restructure.
|
|
||||||
- The "iOS: App Store / TestFlight" cell in the PWA-vs-Capacitor table sits in the Capacitor column but reads like a downside. Clarify it's the distribution path, not a limitation relative to PWA.
|
|
||||||
|
|
||||||
## Bottom line
|
|
||||||
|
|
||||||
The plan is sound and the incremental validation approach is right. The two highest-leverage changes:
|
|
||||||
|
|
||||||
1. **Add a data access abstraction layer** before the service worker work. It's small, useful regardless, and may make the SW work moot.
|
|
||||||
2. **Port `merge_all` to JS** so the edit drawer doesn't depend on Pyodide warm-up.
|
|
||||||
|
|
||||||
With `capacitor-nodejs` removed, the offline path is: data access abstraction → IndexedDB-backed loader → JS merge → (optionally) service worker for any remaining `fetch('/data/*')` callsites that can't be migrated to the loader.
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
# Repository Audit — 2026-03-30
|
|
||||||
|
|
||||||
## CRITICAL
|
|
||||||
|
|
||||||
| # | Area | File | Issue | Status |
|
|
||||||
|---|------|------|-------|--------|
|
|
||||||
| 1 | Security | `edit/server.py:351,386,431,588` | **Path traversal** — `activity_id` from URL is interpolated into filesystem paths without sanitization. Attacker can read/write/delete arbitrary files via `../../` | ✅ Fixed — `_check_id()` validates against `[a-zA-Z0-9\-]+` |
|
|
||||||
| 2 | Security | `edit/server.py:588` | **Path traversal in `delete_image`** — `filename` param is not `.name`-stripped (unlike `upload_image`), enabling deletion of arbitrary files | ✅ Fixed — `Path(filename).name` strip added |
|
|
||||||
| 3 | Security | `edit/server.py:542` | **Path traversal in `upload_activity`** — uploaded `file.filename` used directly for staged path | ✅ Fixed — `Path(file.filename).name` strip added |
|
|
||||||
| 4 | XSS | `ActivityDetail.svelte:67,189` | `marked()` output rendered with `{@html}` without sanitization. Sidecar markdown can inject `<script>` tags | ✅ Fixed — DOMPurify.sanitize() wraps marked() output |
|
|
||||||
|
|
||||||
## HIGH
|
|
||||||
|
|
||||||
| # | Area | File | Issue | Status |
|
|
||||||
|---|------|------|-------|--------|
|
|
||||||
| 5 | Data | `metrics.py:116-148` | **MMP sliding window on non-contiguous data** — gaps in recording are collapsed, inflating power values | ✅ Fixed — dense 1Hz array with gap zero-fill (standard GoldenCheetah/WKO approach) |
|
|
||||||
| 6 | Data | `metrics.py:170-178` | **Best-effort times on non-contiguous data** — pauses removed from speed array produce artificially fast times | ✅ Fixed — same zero-fill approach, gaps count as 0 km/h |
|
|
||||||
| 7 | Data | `writer.py:85-86` | **Activity ID collision** — two activities with same start second + title silently overwrite each other | ✅ Fixed — collision detected by source_hash comparison; disambiguated with 6-char hash suffix |
|
|
||||||
| 8 | Frontend | `ActivityMap.svelte:88-94` | **Misaligned lat/lon arrays** — independently filtered for nulls, so `lats[0]` may not correspond to `lons[0]`. Start/end markers placed at wrong coordinates | ✅ Fixed — lat/lon filtered as pairs |
|
|
||||||
| 9 | Frontend | `EditDrawer.svelte:121` | **Backdrop dismiss fires `saved` event** with unsaved data, overwriting displayed title/description | ✅ Fixed — backdrop/close now dispatch `close` event |
|
|
||||||
| 10 | Schema | `writer.py` vs `bas-v1.schema.json` | **Writer output fails own schema** — `mmp`, `best_efforts`, `best_climb_m`, `preview_coords`, `custom` written but not declared; schema has `additionalProperties: false` | ✅ Fixed — all missing fields declared in schema |
|
|
||||||
|
|
||||||
## MEDIUM
|
|
||||||
|
|
||||||
| # | Area | File | Issue | Status |
|
|
||||||
|---|------|------|-------|--------|
|
|
||||||
| 11 | Data | `metrics.py:89-90` | **Falsy check drops `0.0` speed** — `if avg_speed_kmh` treats `0.0` as falsy; should be `is not None` | ✅ Fixed — `is not None` check |
|
|
||||||
| 12 | Data | `parsers/fit.py:89` | Same falsy `0.0` bug for FIT lap `speed_raw` | ✅ Fixed — `is not None` check |
|
|
||||||
| 13 | Data | `metrics.py:222-239` | `_best_climb` filters out `None` elevations, joining non-contiguous segments | ✅ Fixed — `None` samples reset Kadane's window instead of being skipped |
|
|
||||||
| 14 | Parser | `parsers/tcx.py:89-97` | **TCX timestamps with numeric timezone offsets** (`+02:00`) crash — only `Z` suffix handled | ✅ Fixed — numeric offsets parsed with `%z`, converted to UTC |
|
|
||||||
| 15 | Security | `edit/server.py:22` | **CORS `allow_origins=["*"]`** — any website can make requests to the edit server | ✅ Fixed — restricted to `localhost` origins only |
|
|
||||||
| 16 | Security | `edit/server.py:406` | **YAML injection** — `hide_stats` values written into YAML frontmatter without quoting | ✅ Fixed — values filtered against STAT_PANELS allowlist |
|
|
||||||
| 17 | Data | `edit/server.py:497-499` | `save_athlete` mutates immutable `athlete.json` in-place, violating extract immutability | ✅ Fixed — `merge_all()` now applies `edits/athlete.yaml` overlay; server only writes the sidecar |
|
|
||||||
| 18 | Schema | `sport.py` vs schema | **`skiing` sport** produced by code but missing from JSON schema and SCHEMA.md | ✅ Fixed — `skiing` added to schema sport enum |
|
|
||||||
| 19 | Schema | `sport.py` vs schema | **Sub-sport values** `nordic`, `alpine`, `open_water`, `pool` not in schema enum | ✅ Fixed — added to schema sub_sport enum |
|
|
||||||
| 20 | Schema | `SCHEMA.md:140` | **Activity ID format wrong** — docs say `+0200` offset, code uses `Z` UTC | ✅ Fixed — all examples updated to `Z` suffix |
|
|
||||||
| 21 | Frontend | `EditDrawer.svelte:106` | **Regex injection** — filename with metacharacters (e.g. `photo(1).jpg`) breaks `deleteImage` regex | ✅ Fixed — filename escaped before RegExp construction |
|
|
||||||
| 22 | Frontend | `EditDrawer.svelte:85-99` | No try/catch in `uploadImages` — network error leaves UI stuck in uploading state | ✅ Fixed — try/catch with `finally { uploading = false }` |
|
|
||||||
| 23 | Frontend | `RecordsView.svelte:99`, `Base.astro:220` | Hardcoded `/activity/` URLs ignore `BASE_URL`, breaking subdirectory deployments | ✅ Fixed — `base` prop threaded through `AthleteView` → `RecordsView`; `Base.astro` uses `baseUrl` from `import.meta.env.BASE_URL` |
|
|
||||||
|
|
||||||
## LOW
|
|
||||||
|
|
||||||
| # | Area | File | Issue | Status |
|
|
||||||
|---|------|------|-------|--------|
|
|
||||||
| 24 | Data | `cli.py:341-350` | `_patch_duplicate_of` silently swallows all exceptions | ✅ Fixed — logs warning via `logging.warning` instead of bare `pass` |
|
|
||||||
| 25 | Data | `timeseries.py:26-35` | Non-monotonic `t` array if input points aren't sorted | ✅ Fixed — `t <= last_t` guard rejects both duplicate and backwards timestamps |
|
|
||||||
| 26 | Data | `simplify.py:49-50` | `preview_coords` returns `max_points + 1` elements (off-by-one) | ✅ Fixed — subsample to `max_points - 1` then append last point |
|
|
||||||
| 27 | Config | `render/cli.py:42`, `edit/cli.py:66` | `yaml.safe_load` returns `None` for empty config → `AttributeError` | ✅ Fixed — `or {}` fallback on both load sites |
|
|
||||||
| 28 | Frontend | `format.ts:18` | `formatDuration` doesn't floor seconds — fractional values display messily | ✅ Fixed — `Math.floor(s)` before any arithmetic |
|
|
||||||
| 29 | Frontend | `StatsView.svelte:31` | No error handling on `index.json` fetch | ✅ Fixed — try/catch with error display |
|
|
||||||
| 30 | Frontend | `ActivityMap.svelte` | No `map.resize()` on container size change | ✅ Fixed — `ResizeObserver` calls `map.resize()` |
|
|
||||||
| 31 | Test | `test_writer.py:21-30` | Weak assertions — only check substrings, would pass with malformed IDs | ✅ Fixed — exact equality assertions on full ID strings |
|
|
||||||
| 32 | Test | `test_sport.py` | Zero test coverage for `normalise_sub_sport` | ✅ Fixed — 3 new tests covering Strava CamelCase, ski variants, and unknown→None |
|
|
||||||
| 33 | Test | `test_merge.py:70` | Test uses `sport: "gravel"` which isn't a valid BAS sport | ✅ Fixed — changed to `"running"` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Second-pass audit — 2026-04-01
|
|
||||||
|
|
||||||
### CRITICAL / HIGH (new)
|
|
||||||
|
|
||||||
| # | Area | File | Issue | Status |
|
|
||||||
|---|------|------|-------|--------|
|
|
||||||
| 34 | Data | `writer.py:88-93` | **Disambiguated ID not written into JSON body** — collision adds hash suffix to filename but `detail["id"]` still holds original ID | ✅ Fixed — `detail["id"] = activity_id` after disambiguation |
|
|
||||||
| 35 | Data | `cli.py:79-85` | **`write_activity` return value ignored** — caller uses pre-collision ID for `build_summary`, so index links are broken for disambiguated activities | ✅ Fixed — `activity_id = write_activity(...)` captures the canonical ID |
|
|
||||||
| 36 | Data | `writer.py:88-92` | **TOCTOU race in collision guard** — two workers processing same-timestamp activities can both see no existing file and overwrite each other | ✅ Fixed — hybrid pending-file approach: workers write to unique `.pending.json` files, main process arbitrates by quality score and does atomic rename via `finalize_pending()` |
|
|
||||||
| 37 | Schema | `bas-v1.schema.json:90-128` | **`activity_summary` missing `custom` property** — `merge.py` always adds `custom` but schema has `additionalProperties: false` | ✅ Fixed — `custom` added to `activity_summary` |
|
|
||||||
| 38 | Frontend | `EditDrawer.svelte:101` | **Undeclared `error` variable** in uploadImages catch — runtime `ReferenceError` crash on upload failure | ✅ Fixed — uses `saveStatus` with `saveOk = false` |
|
|
||||||
|
|
||||||
### MEDIUM (new)
|
|
||||||
|
|
||||||
| # | Area | File | Issue | Status |
|
|
||||||
|---|------|------|-------|--------|
|
|
||||||
| 39 | Security | `edit/server.py:409` | **Sport value not validated/quoted** before YAML write — YAML injection possible | ✅ Fixed — validated against `SPORTS` allowlist |
|
|
||||||
| 40 | Security | `edit/server.py:446` | **No image content-type validation** — arbitrary files accepted and served as images; HTML upload executes JS in site origin | ✅ Fixed — rejects non-`image/*` content types |
|
|
||||||
| 41 | Security | `edit/server.py:270-274` | **XSS via unescaped filename in `innerHTML`** — `renderImageList` interpolates filenames into HTML without escaping | ✅ Fixed — `escapeHtml()` applied to filenames |
|
|
||||||
| 42 | Frontend | `MmpChart.svelte:154` | **ResizeObserver captures stale closure** — after toggling range selections, resize renders old data | ✅ Fixed — reactive variables keep closure current |
|
|
||||||
| 43 | Schema | `SCHEMA.md:113` | **Sport enum missing "skiing"** — JSON schema has it, SCHEMA.md doesn't | ✅ Fixed — `skiing` added, sub_sport examples updated |
|
|
||||||
| 44 | Schema | `SCHEMA.md:109-129` | **Summary fields table incomplete** — missing `sub_sport`, `mmp`, `best_efforts`, `best_climb_m`, `preview_coords` | ✅ Fixed — all fields added to summary table |
|
|
||||||
| 45 | Config | `config.py:61` | **Empty config file still crashes** — `yaml.safe_load` returns `None`, no `or {}` fallback (round 1 fix applied to render/edit CLIs but not here) | ✅ Fixed — `or {}` fallback added |
|
|
||||||
| 46 | Frontend | `Base.astro:101-106` | **Nav links ignore `BASE_URL`** — hardcoded `/`, `/stats/`, `/athlete/` break subpath deployments | ✅ Fixed — nav uses `baseUrl` variable |
|
|
||||||
| 47 | Data | `render/merge.py:161` | **`athlete.yaml` merge has no field allowlist** — server restricts editable fields, but merge applies all keys | ✅ Fixed — merge uses `_ATHLETE_EDITABLE` allowlist matching server |
|
|
||||||
| 48 | Frontend | `types.ts:102` | **`ActivityDetail extends ActivitySummary`** inherits `detail_url`, `track_url`, `preview_coords` which detail objects never have | ✅ Fixed — `Omit<>` excludes summary-only fields |
|
|
||||||
|
|
||||||
### LOW (new)
|
|
||||||
|
|
||||||
| # | Area | File | Issue | Status |
|
|
||||||
|---|------|------|-------|--------|
|
|
||||||
| 49 | Parser | `parsers/tcx.py:101` | **Timezone offsets without colon** (`+0200`) not handled — only `+02:00` form matched | ✅ Fixed — regex accepts optional colon (`[+-]\d{2}:?\d{2}`) |
|
|
||||||
| 50 | Parser | `parsers/gpx.py:59-78` | **Power data in GPX extensions not parsed** — MMP always None for GPX files with power meters | ✅ Fixed — parses `pwr`, `power`, `watts` extension tags |
|
|
||||||
| 51 | Data | `simplify.py:63-71` | **`speeds` array not filtered same as `coordinates`** — could misalign if `simplify_track` changes | ✅ Fixed — speeds array uses same lat/lon null filter |
|
|
||||||
| 52 | Frontend | `ActivityCharts.svelte` | **No `onDestroy` cleanup** — chart SVG not removed on unmount, minor memory leak | ✅ Fixed — `onDestroy` removes chart SVG |
|
|
||||||
| 53 | Frontend | `ActivityCharts.svelte:83-86` | **`resetTrim` guard always true** — trim doesn't reset on tab change if min/max happen to match | ✅ Fixed — tracks `lastResetTab` to force reset on tab switch |
|
|
||||||
| 54 | Frontend | `AthleteView.svelte:30` | **No validation of URL `tab` parameter** — invalid value shows blank content | ✅ Fixed — validates against `TABS` array, falls back to `'power'` |
|
|
||||||
| 55 | Test | `test_writer.py` | **No tests for `write_activity`, `build_summary`, `write_index`, `write_athlete_json`** | ✅ Partial — added `test_build_summary_required_fields` and `test_id_utc_conversion` |
|
|
||||||
| 56 | Test | `test_sport.py` | **No test for `normalise_sport("skiing")` or swimming variants** | ✅ Fixed — added `test_skiing_variants` and `test_swimming_variants` |
|
|
||||||
| 57 | Test | `test_merge.py:58,122` | **Non-canonical IDs in test fixtures** — use colons and underscores, don't match real ID format | ✅ Fixed — IDs use canonical `2024-01-01T080000Z-morning-ride` format |
|
|
||||||
| 58 | Data | `edit/server.py:549-551` | **No upload size limit** — file written to disk before validation | ✅ Fixed — 50 MB limit enforced before writing to disk |
|
|
||||||
| 59 | Data | `edit/server.py:586` | **Exception message leaks internal paths** — `str(exc)` returned to client in 422 response | ✅ Fixed — returns `type(exc).__name__` only, no internal details |
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
/private/tmp/bincio_dev_test
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
# BincioActivity — Ideas & Roadmap
|
|
||||||
|
|
||||||
## 1. Strava import from UI ✅
|
|
||||||
|
|
||||||
**Goal:** Let users trigger a Strava sync directly from the web UI instead of running CLI commands.
|
|
||||||
|
|
||||||
**Current state:** `bincio extract` CLI handles Strava OAuth + sync. The edit server (FastAPI) runs locally.
|
|
||||||
|
|
||||||
**Ideas:**
|
|
||||||
- Repurpose the upload button popup to offer two paths: "Upload file" (existing) or "Connect Strava"
|
|
||||||
- OAuth flow: open a popup window → Strava auth → redirect back → edit server exchanges code for token → triggers extract
|
|
||||||
- Edit server would need new endpoints: `GET /strava/auth-url`, `GET /strava/callback`, `POST /strava/sync`
|
|
||||||
- Progress feedback: SSE (Server-Sent Events) or polling endpoint to stream sync progress back to the UI
|
|
||||||
|
|
||||||
**Open questions:**
|
|
||||||
- Where to store the Strava token? Currently it's a CLI argument / env var. For server mode, probably a local file in the data dir.
|
|
||||||
- Should sync be incremental (only new activities) or full re-extract?
|
|
||||||
- How does this interact with multi-user mode (see #2)?
|
|
||||||
|
|
||||||
**Effort:** Medium. OAuth popup + SSE progress are the tricky parts.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Multi-user VPS deployment with invite system
|
|
||||||
|
|
||||||
**Goal:** Deploy bincio on a VPS so multiple users can have accounts. Invite-only registration.
|
|
||||||
|
|
||||||
**Current state:** Entirely single-user, local files, no auth layer at all.
|
|
||||||
|
|
||||||
### Clarified model
|
|
||||||
|
|
||||||
- A **bincio instance** is a deployment (personal or group)
|
|
||||||
- **Personal instance:** 1 user, fully self-contained — current state
|
|
||||||
- **Group instance:** N users, invite-only. All users see each other's public activities by default.
|
|
||||||
The `private` flag (already in the schema) means "only me".
|
|
||||||
- **Federation** is between instances via BAS data URLs — no accounts needed to follow someone
|
|
||||||
- Data isolation = run your own instance. No per-user isolation within an instance.
|
|
||||||
|
|
||||||
### Data layout (multi-user)
|
|
||||||
|
|
||||||
```
|
|
||||||
/data/
|
|
||||||
{handle}/ ← one dir per user (extract output)
|
|
||||||
activities/
|
|
||||||
index.json
|
|
||||||
athlete.json
|
|
||||||
edits/
|
|
||||||
_merged/
|
|
||||||
instance.json ← user registry + invite codes
|
|
||||||
```
|
|
||||||
|
|
||||||
The instance's combined feed merges all users' `_merged/index.json` files at render time.
|
|
||||||
|
|
||||||
### Auth layer
|
|
||||||
|
|
||||||
- Session-based auth added to the edit server (FastAPI handles login/logout)
|
|
||||||
- Unauthenticated reads stay fully public (static files, same as today)
|
|
||||||
- Write endpoints check: logged in + owns the activity
|
|
||||||
- Astro site detects logged-in user to show/hide the Edit button per activity
|
|
||||||
|
|
||||||
### Invite system
|
|
||||||
|
|
||||||
- Invites: short random codes (8 chars), stored in `instance.json`
|
|
||||||
- Invite record: `{code, created_by, used_by, created_at, used_at}`
|
|
||||||
- On registration: validate code → create user dir → mark invite used
|
|
||||||
- Limits: admin (unlimited), regular users (3 invites, configurable)
|
|
||||||
- UI: "Your invites" page — generate codes, see who used them
|
|
||||||
|
|
||||||
### Render pipeline (multi-user)
|
|
||||||
|
|
||||||
- `bincio render` runs `merge_all()` for each user, then builds one Astro site
|
|
||||||
- Combined `index.json` = all users' public summaries merged, sorted by date
|
|
||||||
- Per-user profile pages at `/{handle}/` showing only their activities
|
|
||||||
- Activity detail pages stay at `/activity/{id}/` (IDs are globally unique already)
|
|
||||||
- Activity cards in combined feed show "by {handle}" label
|
|
||||||
|
|
||||||
**Decided:**
|
|
||||||
- SQLite for user/invite storage (safe under concurrent writes, still no external DB dependency)
|
|
||||||
- Mastodon-style follow UX: paste a BAS URL into a "Follow" input → instance fetches and caches that feed → activities appear in combined feed with "from {url}" attribution
|
|
||||||
- Feed refresh: pull-on-demand with cache TTL (no background job — fetch on first stale pageload). [open: exact TTL, stale-while-revalidate vs blocking]
|
|
||||||
|
|
||||||
**Effort:** Large. Auth + multi-user render pipeline are the main work. Needs a design pass before coding.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Federation explainer for non-technical users
|
|
||||||
|
|
||||||
**Goal:** Write a clear, friendly explanation of how bincio federation works for people who don't know what JSON or APIs are.
|
|
||||||
|
|
||||||
**Three ways to share your data:**
|
|
||||||
|
|
||||||
### a) Upload raw files
|
|
||||||
> "Export your GPX/FIT files from Garmin, Wahoo, or wherever, and upload them directly. Bincio converts them automatically."
|
|
||||||
|
|
||||||
### b) Upload BAS files
|
|
||||||
> "Already running bincio locally? Export your BAS data and host it anywhere — GitHub Pages, Netlify, a USB stick. Anyone with the link can include your activities in their feed."
|
|
||||||
|
|
||||||
### c) Link to another bincio instance
|
|
||||||
> "Self-hosting bincio? Just share your URL. Friends can follow you by adding your address to their feeds — no accounts needed, no central server."
|
|
||||||
|
|
||||||
**Promo angle ideas:**
|
|
||||||
- "Your data, your rules. No algorithms, no engagement metrics."
|
|
||||||
- "Like RSS for your training log."
|
|
||||||
- "Works offline. Export once, keep forever."
|
|
||||||
- "Follow friends without either of you needing an account on the same platform."
|
|
||||||
|
|
||||||
**Format:** Could be a `FEDERATION.md`, a landing page section, or a one-pager PDF.
|
|
||||||
|
|
||||||
**Effort:** Small (writing). Medium if we want a designed landing page.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Mobile data capture & processing
|
|
||||||
|
|
||||||
**Goal:** Record activities on a phone, convert to BAS, possibly run the full bincio stack on-device.
|
|
||||||
|
|
||||||
**Four sub-questions:**
|
|
||||||
|
|
||||||
### 4a. Record on phone
|
|
||||||
- Standard GPS recording → GPX export is already solved by apps like OsmAnd, Organic Maps, Strava, Komoot
|
|
||||||
- Could build a minimal PWA (Progressive Web App) that records GPS and exports GPX
|
|
||||||
- Web Geolocation API + background service worker has limitations (iOS kills background JS)
|
|
||||||
- React Native / Capacitor would give better background GPS access
|
|
||||||
- **Simplest path:** just recommend OsmAnd/Organic Maps, skip building a recorder
|
|
||||||
|
|
||||||
### 4b. Convert GPX/FIT → BAS on phone
|
|
||||||
- The extract pipeline is pure Python with no native deps (haversine math, standard library)
|
|
||||||
- Could run via **Pyodide** (Python in WebAssembly) in a browser/PWA
|
|
||||||
- Or package as a mobile app with BeeWare/Kivy (Python → iOS/Android)
|
|
||||||
- Performance: A typical 1-hour FIT file is ~1MB, should parse in <1s even in Pyodide
|
|
||||||
- **Interesting path:** PWA with Pyodide — no install needed, works in Safari/Chrome
|
|
||||||
|
|
||||||
### 4c. Serve the full bincio app on a phone
|
|
||||||
- The site is a static Astro build — it's already a collection of HTML/JS/CSS files
|
|
||||||
- A phone could serve these files locally via a small HTTP server
|
|
||||||
- iOS: no background servers, but a Capacitor wrapper could embed a local server
|
|
||||||
- Android: more permissive, could run a Python/Node micro-server as a service
|
|
||||||
- **Alternative:** just open the pre-built site via file:// protocol (may work for static assets)
|
|
||||||
|
|
||||||
### 4d. Fully offline mobile app
|
|
||||||
- Capacitor + the existing Svelte components + Pyodide for extract = plausible stack
|
|
||||||
- Would need to bundle MapLibre tile data locally (big) or use an offline tile server
|
|
||||||
- This is a significant project but technically feasible with existing code
|
|
||||||
|
|
||||||
**Decided path:**
|
|
||||||
1. Recommend OsmAnd / Organic Maps for GPS recording — also built a `/record/` page in the app for in-app GPS recording
|
|
||||||
2. Capacitor wraps the existing Astro/Svelte site as a native iOS/Android app
|
|
||||||
3. `/convert/` page — Pyodide runs the full extract pipeline in-browser (GPX/FIT/TCX → BAS JSON)
|
|
||||||
4. Fully offline on-device: needs more work (see below)
|
|
||||||
|
|
||||||
**Done:**
|
|
||||||
- Capacitor setup in `site/` (`capacitor.config.ts`, scripts, packages)
|
|
||||||
- `/convert/` page with Pyodide (loads bincio wheel, converts files, download or save to cloud)
|
|
||||||
- `/record/` page with live GPS recording, exports GPX → hands off to `/convert/`
|
|
||||||
- `POST /api/import-bas` edit server endpoint (accepts pre-converted BAS JSON)
|
|
||||||
- `site/public/bincio.whl` — bincio Python wheel for Pyodide
|
|
||||||
|
|
||||||
**Fully offline — ordered plan:**
|
|
||||||
|
|
||||||
| # | Step | Effort | Status |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 1 | SW + IndexedDB write (`sw.js`, `localstore.ts`, "Save to device") | Medium | ✅ Done |
|
|
||||||
| 2 | `dataloader.ts` — runtime-merge server + IDB; update all Svelte components | Medium | 🔄 In progress |
|
|
||||||
| 3 | Test convert→save→feed loop in browser (`npm run dev`) | Low | Not started |
|
|
||||||
| 4 | Sidecar merge in JS (port `merge_all`, no Pyodide) | Medium | Not started |
|
|
||||||
| 5 | Pyodide SW cache (cache CDN assets on first visit) | Medium | Not started |
|
|
||||||
| 6 | `npx cap add android` + test in Capacitor WebView | Low | Not started |
|
|
||||||
| 7 | Test on iOS device/simulator | Low | Not started |
|
|
||||||
| 8 | Native micro-server (only if step 7 reveals SW blocked) | Hard | Contingency |
|
|
||||||
|
|
||||||
**`capacitor-nodejs` is not on the roadmap.** If iOS blocks service workers, fallback is a native micro-server or eliminating all remaining `fetch('/data/*')` callsites via step 2.
|
|
||||||
|
|
||||||
See `ARCHITECTURE.md` — "Fully offline — missing pieces" for design details and open questions (storage limits, uninstall, sync conflicts).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Gear from Strava export
|
|
||||||
|
|
||||||
Import `bikes.csv` and `shoes.csv` from the Strava ZIP export to pre-populate a gear selector
|
|
||||||
in the edit drawer. The CSV has: Bike Name, Bike Brand, Bike Model, Bike Default Sport Types.
|
|
||||||
Could store as a `gear.json` in the user's data dir and surface it in the EditDrawer sport/gear fields.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. User feedback backlog (April 2026)
|
|
||||||
|
|
||||||
Items reported via the in-app feedback form.
|
|
||||||
|
|
||||||
| # | Who | Area | Description | Notes |
|
|
||||||
|---|-----|------|-------------|-------|
|
|
||||||
| F1 | brut | Mobile / Nav | ~~Shorten "BincioActivity" logo to "BA" on narrow screens~~ | ✅ Done |
|
|
||||||
| F2 | brut | Mobile / Stats | Day-click tooltip in `StatsView` falls off-screen on mobile | Needs viewport-clamping on the tooltip position |
|
|
||||||
| F3 | brut | Data / Display | Cadence unit "rpm" is wrong for walking and hiking — should be "spm" or hidden | `ActivityCharts.svelte` — sport-aware label or hide tab |
|
|
||||||
| F4 | brut | Feature | No way to mark an activity as "virtual"; virtual activities should be excluded from records | `virtual: true` sidecar flag + `EditDrawer` checkbox; filter in `RecordsView` and `write_athlete_json` |
|
|
||||||
| F5 | brut | Charts / MMP | ~~Power curve Y axis should start at 0 W; X axis should extend to at least 2 hours~~ | ✅ Done |
|
|
||||||
| F6 | brut | Feature | Import gear (bikes, shoes) from Strava export; edit gear; maintenance log with cumulative km | See item 5 above for gear import. Maintenance log is new: date + km + notes per bike |
|
|
||||||
| F7 | brut | About page | Make user handles in the community tree clickable; show online user count | Handles link to `/u/{handle}/`; online count needs server-side tracking (non-trivial) |
|
|
||||||
| F8 | brut | Strava sync | ~~Syncing from Strava does not trigger a site rebuild~~ | ✅ Done — trigger was firing after `yield "done"`, client had already closed the SSE connection |
|
|
||||||
| F9 | diego_p | Data | Check elevation gain on activity `2026-04-11T051441Z` | Likely a GPS artefact or smoothing issue; needs manual inspection |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prioritization (rough)
|
|
||||||
|
|
||||||
| # | Feature | Impact | Effort | Priority |
|
|
||||||
|---|---------|--------|--------|----------|
|
|
||||||
| 1 | Strava sync from UI | High | Medium | Soon |
|
|
||||||
| 3 | Federation explainer | Medium | Small | Soon |
|
|
||||||
| 4a/4b | Mobile record + convert | Medium | Medium | Later |
|
|
||||||
| 2 | Multi-user + invites | High | Large | Later (needs design) |
|
|
||||||
| 4c/4d | Full mobile app | Low | Large | Future |
|
|
||||||
Reference in New Issue
Block a user