fix edit profile
This commit is contained in:
+170
-1
@@ -15,7 +15,7 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import Cookie, FastAPI, HTTPException, Request, Response
|
||||
from fastapi import Cookie, FastAPI, File, HTTPException, Request, Response, UploadFile
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
@@ -320,6 +320,175 @@ async def post_activity(
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
|
||||
@app.get("/api/activity/{activity_id}/images")
|
||||
async def list_images(
|
||||
activity_id: str,
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
user = _require_user(bincio_session)
|
||||
_check_id(activity_id)
|
||||
dd = _get_data_dir() / user.handle
|
||||
images_dir = dd / "edits" / "images" / activity_id
|
||||
images = sorted(p.name for p in images_dir.iterdir() if p.is_file()) if images_dir.exists() else []
|
||||
return JSONResponse({"images": images})
|
||||
|
||||
|
||||
@app.post("/api/activity/{activity_id}/images")
|
||||
async def upload_image(
|
||||
activity_id: str,
|
||||
file: UploadFile = File(...),
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
user = _require_user(bincio_session)
|
||||
_check_id(activity_id)
|
||||
dd = _get_data_dir() / user.handle
|
||||
if not (dd / "activities" / f"{activity_id}.json").exists():
|
||||
raise HTTPException(404, "Activity not found")
|
||||
if not file.filename:
|
||||
raise HTTPException(400, "No filename")
|
||||
ct = file.content_type or ""
|
||||
if not ct.startswith("image/"):
|
||||
raise HTTPException(400, f"Only image files accepted (got {ct})")
|
||||
images_dir = dd / "edits" / "images" / activity_id
|
||||
images_dir.mkdir(parents=True, exist_ok=True)
|
||||
safe_name = Path(file.filename).name
|
||||
(images_dir / safe_name).write_bytes(await file.read())
|
||||
_trigger_rebuild(user.handle)
|
||||
return JSONResponse({"ok": True, "filename": safe_name})
|
||||
|
||||
|
||||
@app.delete("/api/activity/{activity_id}/images/{filename}")
|
||||
async def delete_image(
|
||||
activity_id: str,
|
||||
filename: str,
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
user = _require_user(bincio_session)
|
||||
_check_id(activity_id)
|
||||
dd = _get_data_dir() / user.handle
|
||||
import shutil
|
||||
safe_name = Path(filename).name
|
||||
target = dd / "edits" / "images" / activity_id / safe_name
|
||||
if target.exists() and target.is_file():
|
||||
target.unlink()
|
||||
if target.parent.exists() and not any(target.parent.iterdir()):
|
||||
shutil.rmtree(target.parent)
|
||||
_trigger_rebuild(user.handle)
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
|
||||
@app.get("/api/athlete")
|
||||
async def get_athlete(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
|
||||
user = _require_user(bincio_session)
|
||||
dd = _get_data_dir() / user.handle
|
||||
athlete_path = dd / "athlete.json"
|
||||
if not athlete_path.exists():
|
||||
raise HTTPException(404, "athlete.json not found — run bincio extract first")
|
||||
data = json.loads(athlete_path.read_text(encoding="utf-8"))
|
||||
# Layer edits/athlete.yaml on top
|
||||
edits_path = dd / "edits" / "athlete.yaml"
|
||||
if edits_path.exists():
|
||||
try:
|
||||
import yaml
|
||||
edits = yaml.safe_load(edits_path.read_text(encoding="utf-8")) or {}
|
||||
for k in ("max_hr", "ftp_w", "hr_zones", "power_zones", "seasons", "gear"):
|
||||
if k in edits:
|
||||
data[k] = edits[k]
|
||||
except Exception:
|
||||
pass
|
||||
return JSONResponse({
|
||||
"max_hr": data.get("max_hr"),
|
||||
"ftp_w": data.get("ftp_w"),
|
||||
"hr_zones": data.get("hr_zones"),
|
||||
"power_zones": data.get("power_zones"),
|
||||
"seasons": data.get("seasons", []),
|
||||
"gear": data.get("gear", {}),
|
||||
})
|
||||
|
||||
|
||||
@app.post("/api/athlete")
|
||||
async def save_athlete(
|
||||
request: Request,
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
user = _require_user(bincio_session)
|
||||
dd = _get_data_dir() / user.handle
|
||||
if not (dd / "athlete.json").exists():
|
||||
raise HTTPException(404, "athlete.json not found — run bincio extract first")
|
||||
payload = await request.json()
|
||||
edits_dir = dd / "edits"
|
||||
edits_dir.mkdir(exist_ok=True)
|
||||
overrides: dict[str, Any] = {}
|
||||
if payload.get("max_hr") is not None:
|
||||
overrides["max_hr"] = int(payload["max_hr"])
|
||||
if payload.get("ftp_w") is not None:
|
||||
overrides["ftp_w"] = int(payload["ftp_w"])
|
||||
if payload.get("hr_zones") is not None:
|
||||
overrides["hr_zones"] = [[int(lo), int(hi)] for lo, hi in payload["hr_zones"]]
|
||||
if payload.get("power_zones") is not None:
|
||||
overrides["power_zones"] = [[int(lo), int(hi)] for lo, hi in payload["power_zones"]]
|
||||
if payload.get("seasons") is not None:
|
||||
overrides["seasons"] = [
|
||||
{"name": str(s["name"]), "start": str(s["start"]), "end": str(s["end"])}
|
||||
for s in payload["seasons"]
|
||||
]
|
||||
if payload.get("gear") is not None:
|
||||
overrides["gear"] = payload["gear"]
|
||||
import yaml
|
||||
(edits_dir / "athlete.yaml").write_text(
|
||||
yaml.dump(overrides, allow_unicode=True, default_flow_style=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
from bincio.render.merge import merge_all
|
||||
merge_all(dd)
|
||||
_trigger_rebuild(user.handle)
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
|
||||
_SUPPORTED_SUFFIXES = {".fit", ".gpx", ".tcx", ".fit.gz", ".gpx.gz", ".tcx.gz"}
|
||||
|
||||
|
||||
@app.post("/api/upload")
|
||||
async def upload_activity(
|
||||
file: UploadFile = File(...),
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
user = _require_user(bincio_session)
|
||||
dd = _get_data_dir() / user.handle
|
||||
name = Path(file.filename or "upload.fit").name
|
||||
p = Path(name.lower())
|
||||
suffix = (p.stem.rsplit(".", 1)[-1].join([".", ".gz"]) if "." in p.stem else ".gz") if p.suffix == ".gz" else p.suffix
|
||||
if suffix not in _SUPPORTED_SUFFIXES:
|
||||
raise HTTPException(400, f"Unsupported file type '{suffix}'")
|
||||
contents = await file.read()
|
||||
if len(contents) > 50 * 1024 * 1024:
|
||||
raise HTTPException(413, "File too large (max 50 MB)")
|
||||
staging = dd / "_uploads"
|
||||
staging.mkdir(exist_ok=True)
|
||||
staged = staging / name
|
||||
staged.write_bytes(contents)
|
||||
try:
|
||||
from bincio.extract.ingest import ingest_parsed
|
||||
from bincio.extract.parsers.factory import parse_file
|
||||
activity = parse_file(staged)
|
||||
activity_id_check = dd / "activities" / f"{activity.source_file}.json"
|
||||
from bincio.extract.writer import make_activity_id
|
||||
activity_id = make_activity_id(activity)
|
||||
if (dd / "activities" / f"{activity_id}.json").exists():
|
||||
raise HTTPException(409, f"Activity already exists: {activity_id}")
|
||||
ingest_parsed(activity, dd, privacy="public")
|
||||
from bincio.render.merge import merge_all
|
||||
merge_all(dd)
|
||||
_trigger_rebuild(user.handle)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise HTTPException(422, f"Failed to process activity: {type(exc).__name__}: {exc}")
|
||||
finally:
|
||||
staged.unlink(missing_ok=True)
|
||||
return JSONResponse({"ok": True, "id": activity_id})
|
||||
|
||||
|
||||
@app.post("/api/strava/sync")
|
||||
async def serve_strava_sync(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
|
||||
user = _require_user(bincio_session)
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
let mounted = false;
|
||||
|
||||
const editUrl = import.meta.env.PUBLIC_EDIT_URL ?? '';
|
||||
const editEnabled = editUrl !== '' || import.meta.env.PUBLIC_EDIT_ENABLED === 'true';
|
||||
|
||||
$: if (mounted) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
@@ -94,7 +95,7 @@
|
||||
>{tab.label}</button>
|
||||
{/each}
|
||||
</nav>
|
||||
{#if editUrl}
|
||||
{#if editEnabled}
|
||||
<button
|
||||
on:click={() => drawerOpen = true}
|
||||
class="mb-2 px-3 py-1.5 text-xs border border-zinc-700 hover:border-zinc-500 text-zinc-400 hover:text-white rounded-md transition-colors"
|
||||
@@ -168,7 +169,7 @@
|
||||
|
||||
{/if}
|
||||
|
||||
{#if drawerOpen && editUrl}
|
||||
{#if drawerOpen && editEnabled}
|
||||
<AthleteDrawer
|
||||
{editUrl}
|
||||
on:close={() => drawerOpen = false}
|
||||
|
||||
Reference in New Issue
Block a user