diff --git a/bincio/serve/server.py b/bincio/serve/server.py index 4c74fbf..9a94e70 100644 --- a/bincio/serve/server.py +++ b/bincio/serve/server.py @@ -243,16 +243,19 @@ def _trigger_rebuild(handle: str) -> None: def _run() -> None: try: if _webroot is None: - # Fast: only update data, skip Astro build - subprocess.run( - [uv, "run", "bincio", "render", - "--data-dir", _data_dir, - "--site-dir", _site_dir, - "--handle", _handle, - "--no-build"], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) + # Fast: only update data, skip Astro build. + # Serialised with the same lock: merge_all wipes and recreates + # _merged/activities/ — concurrent runs would corrupt each other. + with _rebuild_lock: + subprocess.run( + [uv, "run", "bincio", "render", + "--data-dir", _data_dir, + "--site-dir", _site_dir, + "--handle", _handle, + "--no-build"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) else: # Full build + rsync — serialised so concurrent uploads don't race with _rebuild_lock: @@ -500,6 +503,20 @@ async def admin_disk(bincio_session: Optional[str] = Cookie(default=None)) -> JS }) +@app.post("/api/admin/users/{handle}/rebuild") +async def admin_rebuild( + handle: str, + bincio_session: Optional[str] = Cookie(default=None), +) -> JSONResponse: + """Trigger a merge_all + site rebuild for a user. Admin only.""" + _require_admin(bincio_session) + user_dir = _get_data_dir() / handle + if not user_dir.is_dir(): + raise HTTPException(404, f"No data directory for user '{handle}'") + _trigger_rebuild(handle) + return JSONResponse({"ok": True}) + + @app.delete("/api/admin/users/{handle}/activities") async def admin_delete_activities( handle: str, diff --git a/site/src/pages/admin/index.astro b/site/src/pages/admin/index.astro index 9070c77..c67ac33 100644 --- a/site/src/pages/admin/index.astro +++ b/site/src/pages/admin/index.astro @@ -115,15 +115,50 @@ import Base from '../../layouts/Base.astro';