From d7fd585e7716b4a9468e245fb78b4594e0403b8e Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Wed, 13 May 2026 08:17:18 +0200 Subject: [PATCH] Add global segment rescan: POST /api/me/segment-rescan + Rescan all button --- bincio/serve/server.py | 58 +++++++++++++++++++++++++ site/src/components/SegmentsView.svelte | 35 +++++++++++++-- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/bincio/serve/server.py b/bincio/serve/server.py index 76a9651..12bf2cb 100644 --- a/bincio/serve/server.py +++ b/bincio/serve/server.py @@ -2584,6 +2584,64 @@ async def trigger_detect( return JSONResponse({"ok": True, "efforts_found": total}) +@app.post("/api/me/segment-rescan") +async def me_segment_rescan( + bincio_session: Optional[str] = Cookie(default=None), +) -> JSONResponse: + """Retroactively detect efforts for ALL segments across ALL activities for the logged-in user.""" + user = _require_user(bincio_session) + dd = _get_data_dir() + user_dir = dd / user.handle + acts_dir = user_dir / "activities" + + from datetime import datetime as _datetime + from bincio.segments.detect import track_from_timeseries_json, detect_one + import json as _json + + segments = _seg_store.list_segments(dd) + if not segments: + return JSONResponse({"ok": True, "efforts_found": 0}) + + total = 0 + for detail_path in sorted(acts_dir.glob("*.json")): + if ".timeseries." in detail_path.name: + continue + try: + detail = _json.loads(detail_path.read_text(encoding="utf-8")) + except Exception: + continue + ts_url = detail.get("timeseries_url") + if not ts_url: + continue + ts_path = user_dir / ts_url + if not ts_path.exists(): + continue + try: + ts = _json.loads(ts_path.read_text(encoding="utf-8")) + except Exception: + continue + started_raw = detail.get("started_at") + if not started_raw: + continue + try: + started_at = _datetime.fromisoformat(started_raw.replace("Z", "+00:00")) + except Exception: + continue + track = track_from_timeseries_json( + ts, detail.get("id", detail_path.stem), + detail.get("sport", "other"), started_at, + ) + if track is None: + continue + for seg in segments: + efforts = detect_one(track, seg) + for effort in efforts: + _seg_store.add_effort(dd, user.handle, seg.id, effort) + total += len(efforts) + + return JSONResponse({"ok": True, "efforts_found": total}) + + @app.get("/api/activities/{activity_id}/segment_efforts") async def activity_segment_efforts( activity_id: str, diff --git a/site/src/components/SegmentsView.svelte b/site/src/components/SegmentsView.svelte index d0a2173..cb5c326 100644 --- a/site/src/components/SegmentsView.svelte +++ b/site/src/components/SegmentsView.svelte @@ -23,6 +23,8 @@ let loading = false; let selectedId: string | null = null; let fetchTimer: ReturnType | null = null; + let rescanning = false; + let rescanMsg: string | null = null; const TILE_STYLE = 'https://tiles.openfreemap.org/styles/positron'; const SEG_COLOR = '#f59e0b'; @@ -123,6 +125,20 @@ ['case', ['==', ['get', 'id'], selectedId ?? ''], 5, 3]); } + async function rescanAll() { + rescanning = true; + rescanMsg = null; + try { + const r = await fetch('/api/me/segment-rescan', { method: 'POST', credentials: 'include' }); + const d = await r.json(); + if (r.ok) rescanMsg = `Found ${d.efforts_found} effort${d.efforts_found !== 1 ? 's' : ''}.`; + else rescanMsg = d.detail ?? 'Rescan failed.'; + } catch { + rescanMsg = 'Could not reach server.'; + } + rescanning = false; + } + async function deleteSegment(id: string) { if (!confirm('Delete this segment? This cannot be undone.')) return; try { @@ -144,10 +160,21 @@

Segments

- - + New segment - +
+ {#if rescanMsg} + {rescanMsg} + {/if} + + + + New segment + +