diff --git a/segments-plan.md b/segments-plan.md deleted file mode 100644 index fc323af..0000000 --- a/segments-plan.md +++ /dev/null @@ -1,232 +0,0 @@ -# Bincio Segments — Design Plan - -## Overview - -Segments are named GPS stretches that can be defined by any user. Every time an activity passes through a segment, an "effort" is recorded automatically. Users can track their personal history and PRs on each segment. - ---- - -## Data Model - -### Segment (global, shared) - -File: `/var/bincio/segments/{segment-id}.json` - -```json -{ - "id": "climb-to-passo-rolle-a3f2", - "name": "Climb to Passo Rolle", - "sport": "cycling", - "polyline": [[46.123, 11.456], [46.124, 11.457], "..."], - "distance_m": 4320.5, - "bbox": [11.456, 46.120, 11.502, 46.135], - "created_by": "brut", - "created_at": "2026-05-13T10:00:00Z" -} -``` - -Segment ID: slugified name + 4-char hash to avoid collisions. - -### SegmentEffort (per user) - -File: `/var/bincio/data/{handle}/segment_efforts/{segment-id}.json` - -Array of efforts, newest first: - -```json -[ - { - "activity_id": "2026-04-26T053013Z-ccbas-...", - "started_at": "2026-04-26T06:12:33Z", - "elapsed_s": 1842, - "avg_speed_kmh": 12.4, - "avg_hr_bpm": 158, - "avg_power_w": 245, - "np_power_w": 261, - "detected_at": "2026-05-13T10:05:00Z" - } -] -``` - ---- - -## Detection Algorithm - -**Input:** activity GPS track (lat/lon, ~1 Hz), segment polyline - -**Steps:** - -1. **Bbox pre-filter** — skip if activity bbox doesn't overlap segment bbox (padded by ~50 m). O(1) per segment. - -2. **Start proximity** — find all activity points within 25 m of `polyline[0]`. Each is a candidate entry point. - -3. **End proximity** — from each candidate entry, scan forward (never backward) for an activity point within 25 m of `polyline[-1]`. Enforces correct direction. - -4. **Path conformance** — between entry and exit indices, project each activity point onto the nearest segment polyline segment and compute perpendicular distance. If more than 30% of points exceed 50 m deviation, reject the match. Configurable thresholds. - -5. **Effort extraction** — from the matched entry/exit indices, slice the timeseries and compute `elapsed_s` and avg metrics. - -6. **Multiple efforts** — after a successful match, continue scanning from the exit point to detect repeat efforts in the same activity (e.g. repeat climbs). - -**Configurable constants** (easy to tune): -- `MATCH_RADIUS_M = 25` — proximity to start/end points -- `CONFORMANCE_MAX_DEVIATION_M = 50` — max perpendicular distance -- `CONFORMANCE_MAX_FRACTION = 0.30` — fraction of points allowed to exceed max deviation - ---- - -## Server-side Code Structure - -``` -bincio/segments/ - __init__.py - models.py — Segment, SegmentEffort dataclasses - store.py — read/write segments and efforts to/from /var/bincio - detect.py — matching algorithm - cli.py — bincio segments detect [--activity X] [--segment Y] [--user U] [--all] -``` - -Detection is called: -- **At ingest time** — after each new activity is processed, reads all segment definitions (bbox pre-filter makes this fast), writes matches to the user's `segment_efforts/` -- **Retroactively** — via CLI or API endpoint, for one user re-running all activities against all (or one) segment(s) - ---- - -## API Endpoints - -All new, added to `server.py`: - -``` -GET /api/segments?bbox=lon_min,lat_min,lon_max,lat_max list segments in viewport -POST /api/segments create segment (auth required) -DELETE /api/segments/{id} delete (created_by or admin only) - -GET /api/segments/{id}/efforts current user's efforts (auth) -POST /api/segments/{id}/detect retrigger detection for current user -``` - -Bbox filtering is done in memory at request time. All segment definitions are read from disk on startup (or lazily cached). Works fine for hundreds of segments. - ---- - -## Frontend - -### New pages / routes - -| Route | Purpose | -|---|---| -| `/segments/` | Map + list of all segments; entry point for browsing and creating | -| `/segments/new/` | Segment creation flow | -| `/segments/{id}/` | Segment detail: polyline on map + user's effort history | - -### `/segments/` page - -- Full-width map showing segment polylines for segments whose bbox overlaps the current map viewport -- Segment list below (or sidebar): name, sport, distance, creator -- "New segment" button → `/segments/new/` -- Clicking a segment → `/segments/{id}/` - -### `/segments/new/` — creation flow - -1. Searchable list of the user's own activities with GPS → select one -2. Activity track shown on map -3. Dual-handle range slider below the map, one position per GPS point in the track - - Moving handles highlights the selected portion on the map in a contrasting colour - - Start and end coordinates are the GPS points at the slider handle positions -4. Name input + optional sport selector -5. Confirm → `POST /api/segments` - -### `/segments/{id}/` — segment detail - -- Segment polyline on map -- Metadata: name, sport, distance, created by -- Effort history table (sorted by date, newest first): - - Date, elapsed time, avg speed, avg HR, avg power, NP - - Delta vs. personal best highlighted -- "Retrigger detection" button → `POST /api/segments/{id}/detect` - -### Activity detail page (existing) - -Below the stats table: if any segment efforts were detected for this activity, show a compact list — segment name + elapsed time + Δ vs. PR. - -### Athlete page (existing) - -New "Segments" section: list of segments the user has efforts on, showing best time and effort count for each. - ---- - -## Segment Creation UX (refined) - -Standalone `/segments/new/` page with an activity picker. Additionally, the activity detail page gets a **"Create segment from this activity"** button that links to `/segments/new/?activity={id}`, pre-selecting the activity and skipping the picker step. - -Rationale: users will typically think "I want to turn this climb I just did into a segment" — the shortcut from activity detail makes that fast, while the standalone page remains the canonical entry point for discovery. - ---- - -## Implementation Strategy - -UI-first, so users can start populating the segment collection immediately. Detection comes after. - -**Phase 1 — Minimal backend to unblock UI** ✅ -1. `bincio/segments/models.py` — Segment and SegmentEffort dataclasses ✅ -2. `bincio/segments/store.py` — read/write segments to/from disk ✅ -3. `POST /api/segments` + `GET /api/segments?bbox=...` + `DELETE /api/segments/{id}` ✅ - -**Phase 2 — Creation UI** ✅ -4. `/segments/new/` page — activity picker → map + dual-handle slider → name/save ✅ -5. `/segments/` page — map + list of existing segments, bbox-filtered ✅ -6. Activity detail — "Create segment from this activity" shortcut button ✅ - -**Phase 3 — Detection** ✅ -7. `bincio/segments/detect.py` — matching algorithm, tested in isolation ✅ -8. `bincio/segments/cli.py` — retroactive detection CLI (`bincio segments detect`) ✅ -9. Ingest hook — run detection at end of each ingest ✅ -10. `POST /api/segments/{id}/detect` + `GET /api/segments/{id}/efforts` ✅ - -**Phase 4 — Display** (next) -11. `/segments/{id}/` page — segment detail + effort history table -12. Activity detail — show matched segment efforts below stats -13. Athlete page — segments section - ---- - -## Phase 4 Detail - -### New API endpoints - -| Endpoint | Notes | -|---|---| -| `GET /api/segments/{id}` | Single segment metadata | -| `GET /api/activities/{activity_id}/segment_efforts` | Scans user's effort files; returns `[{segment_id, segment_name, elapsed_s, started_at, ...}]` for hits on this activity. Auth required. | -| `GET /api/users/{handle}/segment_summary` | Public. Returns `[{segment: {...}, best_elapsed_s, effort_count}]` — all segments where this user has efforts, best time + count each. | - -### SegmentDetail page (`/segments/{id}/`) - -- Map with segment polyline (reuse MapLibre, same style as `SegmentsView`) -- Metadata row: name, sport, distance, created by -- Effort history table sorted newest-first; columns: Date, Time, Avg speed, Avg HR, Avg power, NP, Δ vs PR - - PR row highlighted; Δ column shows `+N s` / `-N s` in red/green vs best effort -- "Retrigger detection" button with loading state (blocks, shows count when done) -- Auth-gated: effort table only rendered if logged in - -### Activity detail additions - -Below the laps table: if the user is logged in and this activity has any segment effort hits, show a compact block — segment name (linked to `/segments/{id}/`) + elapsed time + Δ vs PR. - -Fetch: `GET /api/activities/{activity_id}/segment_efforts` (gracefully hidden if 401 or empty). - -### Athlete page additions - -New "Segments" tab in `AthleteView.svelte`. Fetches `GET /api/users/{handle}/segment_summary` (public, no auth needed). Shows a table: Segment name (linked), Sport, Distance, Best time, # efforts. Empty state if no efforts yet. - -### Effort time display - -Use compact `m:ss` format (e.g. "5:23") for segment effort tables — add `formatElapsed(s: number): string` to `format.ts`. - -### Post-save UX in SegmentCreate (updated) - -After a successful save, instead of immediately redirecting to `/segments/`, show an inline prompt: -> Segment saved! Add another from this activity? [Yes, add another] [Done] - -- **"Yes, add another"** — clears `segName`, `segSport`; resets handles to full range; keeps the map and activity loaded -- **"Done"** — navigates to `/segments/`