ui: loading states, player failure overlay + retry, toasts, back nav, full history page with delete

Bundle 1-4+7 from PLAN.md: skeleton loaders, player Loading/Buffering/fatal overlay with Retry, action toasts, back-to-home crumb + logo fix, ?history full-history view with per-entry delete (GET /api/history?all=1, DELETE /api/history). 14/14 tests green.
This commit is contained in:
jozamudi committed 2026-08-19 18:20:42 +00:00
1 parent 19e878800c
commit 2ec0f8a034
10 files changed
+414 -20

No files matched your search

+17 -4
View File
@@ -139,10 +139,23 @@ def _save_hist(user: str, entries):
@app.get("/api/history")
def api_history(request: Request):
# no-store: this JSON mutates while a tab is open (position saves);
# a stale copy has caused "poster gone" confusion before
return JSONResponse(
sorted(_load_hist(request.state.user), key=lambda e: e.get("t", 0), reverse=True)[:5],
headers={"Cache-Control": "no-store"})
# a stale copy has caused "poster gone" confusion before.
# ?all=1: full list for the /history page (no top-5 cap)
entries = sorted(_load_hist(request.state.user), key=lambda e: e.get("t", 0), reverse=True)
if request.query_params.get("all") != "1":
entries = entries[:5]
return JSONResponse(entries, headers={"Cache-Control": "no-store"})
@app.delete("/api/history")
def api_history_delete(request: Request, slug_id: str, ep_id: str):
# loose str() match on purpose: legacy rows store the same ep_id as int
# AND as string — one delete clears both ghost rows
entries = _load_hist(request.state.user)
keep = [e for e in entries
if not (str(e.get("slug_id")) == slug_id and str(e.get("ep_id")) == ep_id)]
_save_hist(request.state.user, keep)
return {"removed": len(entries) - len(keep)}
@app.post("/api/history")