fix edit profile
This commit is contained in:
+170
-1
@@ -15,7 +15,7 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
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.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
@@ -320,6 +320,175 @@ async def post_activity(
|
|||||||
return JSONResponse({"ok": True})
|
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")
|
@app.post("/api/strava/sync")
|
||||||
async def serve_strava_sync(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
|
async def serve_strava_sync(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
|
||||||
user = _require_user(bincio_session)
|
user = _require_user(bincio_session)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
let mounted = false;
|
let mounted = false;
|
||||||
|
|
||||||
const editUrl = import.meta.env.PUBLIC_EDIT_URL ?? '';
|
const editUrl = import.meta.env.PUBLIC_EDIT_URL ?? '';
|
||||||
|
const editEnabled = editUrl !== '' || import.meta.env.PUBLIC_EDIT_ENABLED === 'true';
|
||||||
|
|
||||||
$: if (mounted) {
|
$: if (mounted) {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
@@ -94,7 +95,7 @@
|
|||||||
>{tab.label}</button>
|
>{tab.label}</button>
|
||||||
{/each}
|
{/each}
|
||||||
</nav>
|
</nav>
|
||||||
{#if editUrl}
|
{#if editEnabled}
|
||||||
<button
|
<button
|
||||||
on:click={() => drawerOpen = true}
|
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"
|
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}
|
||||||
|
|
||||||
{#if drawerOpen && editUrl}
|
{#if drawerOpen && editEnabled}
|
||||||
<AthleteDrawer
|
<AthleteDrawer
|
||||||
{editUrl}
|
{editUrl}
|
||||||
on:close={() => drawerOpen = false}
|
on:close={() => drawerOpen = false}
|
||||||
|
|||||||
Reference in New Issue
Block a user