diff --git a/bincio/serve/server.py b/bincio/serve/server.py index 723f641..fba81ce 100644 --- a/bincio/serve/server.py +++ b/bincio/serve/server.py @@ -509,6 +509,8 @@ async def admin_disk(bincio_session: Optional[str] = Cookie(default=None)) -> JS return 0 return sum(1 for f in path.glob(pattern) if f.is_file()) + db = _get_db() + from bincio.serve.db import get_user as _get_user users = [] for user_dir in sorted(data_dir.iterdir()): if not user_dir.is_dir() or user_dir.name.startswith("_"): @@ -517,6 +519,7 @@ async def admin_disk(bincio_session: Optional[str] = Cookie(default=None)) -> JS leaked = [f for f in user_dir.glob("tmp*.zip") if f.is_file()] users.append({ "handle": user_dir.name, + "in_db": _get_user(db, user_dir.name) is not None, "total_mb": _mb(user_dir), "activities_mb": _mb(user_dir / "activities"), "activities_count": _count(user_dir / "activities", "*.json"), @@ -850,6 +853,38 @@ async def admin_delete_activities( return JSONResponse({"ok": True, "deleted": deleted}) +@app.delete("/api/admin/users/{handle}/directory") +async def admin_delete_user_directory( + handle: str, + bincio_session: Optional[str] = Cookie(default=None), +) -> JSONResponse: + """Delete the entire user directory from disk (for ghost users not in the DB). + + Refuses if the handle exists as an account in the database — use + DELETE /api/admin/users/{handle}/activities for registered users. + """ + import shutil + _require_admin(bincio_session) + db = _get_db() + from bincio.serve.db import get_user as _get_user + if _get_user(db, handle) is not None: + raise HTTPException( + 400, + f"User '{handle}' is still in the database. Remove the account first, " + "or use 'Reset data' to wipe only activity files.", + ) + user_dir = _get_data_dir() / handle + if not user_dir.is_dir(): + raise HTTPException(404, f"No directory for '{handle}'") + shutil.rmtree(user_dir) + # Rebuild root manifest so the ghost shard disappears from the site + from bincio.render.cli import _write_root_manifest + try: + _write_root_manifest(_get_data_dir()) + except Exception: + pass + return JSONResponse({"ok": True}) + # ── Write API (ported from bincio edit, auth-gated) ─────────────────────────── diff --git a/site/src/pages/admin/index.astro b/site/src/pages/admin/index.astro index bfa792d..25124ff 100644 --- a/site/src/pages/admin/index.astro +++ b/site/src/pages/admin/index.astro @@ -123,32 +123,14 @@ import Base from '../../layouts/Base.astro'; const leaked = u.leaked_zips_count > 0 ? `⚠ ${fmt(u.leaked_zips_mb)} leaked` : ''; + const ghostBadge = !u.in_db + ? `ghost` + : ''; const stravaNote = u.originals_strava_mb > 0 ? `(${fmt(u.originals_strava_mb)} Strava)` : ''; - return ` -