Segments Phase 4: detail page, activity efforts, athlete tab, new APIs
New API endpoints:
- GET /api/segments/{id} — single segment metadata
- GET /api/activities/{id}/segment_efforts — efforts for an activity (auth)
- GET /api/users/{handle}/segment_summary — public best time + count per segment
New components:
- SegmentDetail.svelte — map + metadata + effort table (with PR/Δ) + rescan button
- SegmentsPage.svelte — URL router: shows detail when /segments/{id}/, list otherwise
Updated:
- segments/index.astro — now uses SegmentsPage router
- nginx-activity.conf — add /segments/ try_files rule for client-side routing
- ActivityDetail.svelte — segment efforts block below laps
- AthleteView.svelte — Segments tab with best time + effort count per segment
- format.ts — add formatElapsed() for compact m:ss display
This commit is contained in:
@@ -2437,6 +2437,25 @@ async def get_segments(
|
||||
} for s in segs])
|
||||
|
||||
|
||||
@app.get("/api/segments/{segment_id}")
|
||||
async def get_segment(segment_id: str) -> JSONResponse:
|
||||
"""Return metadata for a single segment."""
|
||||
dd = _get_data_dir()
|
||||
seg = _seg_store.load_segment(dd, segment_id)
|
||||
if seg is None:
|
||||
raise HTTPException(404, "Segment not found")
|
||||
return JSONResponse({
|
||||
"id": seg.id,
|
||||
"name": seg.name,
|
||||
"sport": seg.sport,
|
||||
"polyline": seg.polyline,
|
||||
"distance_m": seg.distance_m,
|
||||
"bbox": seg.bbox,
|
||||
"created_by": seg.created_by,
|
||||
"created_at": _seg_store._iso(seg.created_at),
|
||||
})
|
||||
|
||||
|
||||
@app.post("/api/segments")
|
||||
async def create_segment(
|
||||
body: CreateSegmentRequest,
|
||||
@@ -2565,6 +2584,70 @@ async def trigger_detect(
|
||||
return JSONResponse({"ok": True, "efforts_found": total})
|
||||
|
||||
|
||||
@app.get("/api/activities/{activity_id}/segment_efforts")
|
||||
async def activity_segment_efforts(
|
||||
activity_id: str,
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
"""Return segment efforts that belong to a specific activity for the logged-in user."""
|
||||
user = _require_user(bincio_session)
|
||||
dd = _get_data_dir()
|
||||
efforts_dir = dd / user.handle / "segment_efforts"
|
||||
result = []
|
||||
if efforts_dir.exists():
|
||||
import json as _json
|
||||
for ef_file in sorted(efforts_dir.glob("*.json")):
|
||||
seg_id = ef_file.stem
|
||||
all_efforts = _seg_store.load_efforts(dd, user.handle, seg_id)
|
||||
matching = [e for e in all_efforts if e.activity_id == activity_id]
|
||||
if not matching:
|
||||
continue
|
||||
seg = _seg_store.load_segment(dd, seg_id)
|
||||
if not seg:
|
||||
continue
|
||||
pr_elapsed = min(e.elapsed_s for e in all_efforts)
|
||||
for eff in matching:
|
||||
result.append({
|
||||
"segment_id": seg.id,
|
||||
"segment_name": seg.name,
|
||||
"segment_distance_m": seg.distance_m,
|
||||
"elapsed_s": eff.elapsed_s,
|
||||
"pr_elapsed_s": pr_elapsed,
|
||||
"started_at": _seg_store._iso(eff.started_at),
|
||||
})
|
||||
return JSONResponse(result)
|
||||
|
||||
|
||||
@app.get("/api/users/{handle}/segment_summary")
|
||||
async def user_segment_summary(handle: str) -> JSONResponse:
|
||||
"""Public endpoint: segments where this user has efforts, with best time and count."""
|
||||
dd = _get_data_dir()
|
||||
efforts_dir = dd / handle / "segment_efforts"
|
||||
result = []
|
||||
if efforts_dir.exists():
|
||||
for ef_file in sorted(efforts_dir.glob("*.json")):
|
||||
seg_id = ef_file.stem
|
||||
efforts = _seg_store.load_efforts(dd, handle, seg_id)
|
||||
if not efforts:
|
||||
continue
|
||||
seg = _seg_store.load_segment(dd, seg_id)
|
||||
if not seg:
|
||||
continue
|
||||
best = min(efforts, key=lambda e: e.elapsed_s)
|
||||
result.append({
|
||||
"segment": {
|
||||
"id": seg.id,
|
||||
"name": seg.name,
|
||||
"sport": seg.sport,
|
||||
"distance_m": seg.distance_m,
|
||||
},
|
||||
"best_elapsed_s": best.elapsed_s,
|
||||
"effort_count": len(efforts),
|
||||
})
|
||||
result.sort(key=lambda x: x["segment"]["name"].lower())
|
||||
return JSONResponse(result)
|
||||
|
||||
|
||||
# ── Feedback ──────────────────────────────────────────────────────────────────
|
||||
|
||||
_FEEDBACK_IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".heic"}
|
||||
|
||||
Reference in New Issue
Block a user