Fix elevation gain inflation from device no-fix leading zeros
Apple Watch and similar devices record exactly 0.0 for elevation while
waiting for barometric/GPS lock, then jump to the real altitude. The
hysteresis accumulator was seeding from 0.0, counting the full jump as
ascent. Fix: detect a leading near-zero run followed by a large jump
and seed the accumulator from the first real value instead.
Applied in both _elevation() (fresh extractions) and
recalculate_elevation_hysteresis() (recompute path). Added a bulk
admin endpoint POST /api/admin/users/{handle}/recompute-elevation and
corresponding button to fix existing stored activities.
This commit is contained in:
@@ -1900,6 +1900,58 @@ async def recalculate_elevation_hysteresis_endpoint(
|
||||
raise HTTPException(422, str(e))
|
||||
|
||||
|
||||
@app.post("/api/admin/users/{handle}/recompute-elevation")
|
||||
async def admin_recompute_elevation(
|
||||
handle: str,
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
"""Recompute elevation gain/loss for all activities of a user from stored timeseries.
|
||||
|
||||
Skips activities with altitude_source == 'dem' (already DEM-corrected).
|
||||
Applies the leading-zero no-fix fix and source-aware hysteresis.
|
||||
Returns patched/skipped/error counts.
|
||||
"""
|
||||
_require_admin(bincio_session)
|
||||
user_dir = _get_data_dir() / handle
|
||||
if not user_dir.is_dir():
|
||||
raise HTTPException(404, f"No data directory for '{handle}'")
|
||||
|
||||
from bincio.extract.dem import recalculate_elevation_hysteresis
|
||||
from bincio.render.merge import merge_one
|
||||
|
||||
patched = skipped = errors = 0
|
||||
acts_dir = user_dir / "activities"
|
||||
for json_path in sorted(acts_dir.glob("*.json")):
|
||||
if json_path.name.endswith(".timeseries.json"):
|
||||
continue
|
||||
activity_id = json_path.stem
|
||||
try:
|
||||
detail = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
if detail.get("altitude_source") == "dem":
|
||||
skipped += 1
|
||||
continue
|
||||
ts_path = acts_dir / f"{activity_id}.timeseries.json"
|
||||
if not ts_path.exists():
|
||||
skipped += 1
|
||||
continue
|
||||
ts = json.loads(ts_path.read_text(encoding="utf-8"))
|
||||
ele_arr = ts.get("elevation_m") or []
|
||||
if not any(e for e in ele_arr if e is not None):
|
||||
skipped += 1
|
||||
continue
|
||||
recalculate_elevation_hysteresis(user_dir, activity_id)
|
||||
merge_one(user_dir, activity_id)
|
||||
patched += 1
|
||||
except Exception as exc:
|
||||
log.warning("recompute-elevation[%s/%s]: %s", handle, activity_id, exc)
|
||||
errors += 1
|
||||
|
||||
if patched > 0:
|
||||
_trigger_rebuild(handle)
|
||||
|
||||
return JSONResponse({"ok": True, "patched": patched, "skipped": skipped, "errors": errors})
|
||||
|
||||
|
||||
@app.delete("/api/activity/{activity_id}", response_model=GenericResponse)
|
||||
async def delete_activity(
|
||||
activity_id: str,
|
||||
|
||||
Reference in New Issue
Block a user