Status pill + git-SHA asset versioning

- _origin_probe(): 3-min check of anidb.app pages/API; OK/DOWN + reason
  in GET /api/cdn and server.log ([probe] ORIGIN ...)
- header pill: upstream-site dot (red = down) + CDN dot + label;
  per-dot hover tooltips, tap -> toast legend (mobile has no hover)
- ?v={{VERSION}} injected from 'git describe --always --dirty' at
  startup; JSV reads it from its own script URL - replaces hand-bumped
  numbers (JSV/?v drift) and gives play-probes the running commit
This commit is contained in:
jozamudi committed 2026-09-03 16:33:51 +00:00
1 parent 3f715434f0
commit ff8311c1a8
4 files changed
+178 -59

No files matched your search

+53 -3
View File
@@ -7,6 +7,7 @@ Cloudflare workaround) and rewrites playlist URIs to go through itself.
import base64
import json
import os
import subprocess
import threading
import time
from collections import Counter, OrderedDict
@@ -16,7 +17,7 @@ from base64 import urlsafe_b64encode, urlsafe_b64decode
from urllib.parse import quote, urljoin, urlparse
from fastapi import Body, FastAPI, HTTPException, Query, Request
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse, Response, StreamingResponse
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse, Response, StreamingResponse
from fastapi.staticfiles import StaticFiles
from itsdangerous import BadSignature, TimestampSigner
@@ -25,8 +26,28 @@ from . import db, upstream, users
ROOT = Path(__file__).parent.parent
app = FastAPI(title="ani-web")
def _version():
"""Cache-bust token: git short SHA (+ -dirty). The content of a commit
never changes, so ?v=<sha> is a correct cache key — this replaces the
hand-bumped ?v= number (forgetting a bump = users stuck on stale JS).
Every deploy restarts the process (run.sh), so the read is fresh."""
try:
v = subprocess.run(["git", "describe", "--always", "--dirty"], cwd=ROOT,
capture_output=True, text=True, timeout=5).stdout.strip()
if v:
return v
except Exception:
pass
return "dev"
VERSION = _version()
# latest probe result + last 20 samples (~1h at 3-min spacing), for GET /api/cdn
_CDN = {"state": "unknown", "kbps": 0.0, "ts": 0.0, "hist": []}
# origin (anidb.app pages/API) health — separate host, separate failure mode
# (observed 2026-09-03: CDN fine while origin 503'd "Under Maintenance")
_ORIGIN = {"state": "unknown", "msg": "", "ts": 0.0}
def _cdn_probe():
@@ -62,16 +83,34 @@ def _cdn_probe():
_CDN.update(state=now, kbps=round(kbps), ts=time.time())
_CDN["hist"].append({"t": time.time(), "kbps": round(kbps)})
del _CDN["hist"][:-20]
_origin_probe()
time.sleep(180)
def _origin_probe():
"""Hit the same kind of page users load (details page) every 3 min and
track OK/DOWN — this is the host that 503s 'Under Maintenance'.
ponytail: one URL, no history; the log line + ts tell the when."""
try:
r = upstream._get("https://anidb.app/anime/jujutsu-kaisen-season-2-2554",
timeout=10)
state, msg = "OK", f"http {r.status_code}"
except Exception as e:
state, msg = "DOWN", str(e)
prev = _ORIGIN["state"]
_ORIGIN.update(state=state, msg=msg, ts=time.time())
if state != prev:
print(f"[probe] ORIGIN {state} ({msg})", flush=True)
threading.Thread(target=_cdn_probe, daemon=True).start()
@app.get("/api/cdn")
async def cdn_status():
"""CDN health (read-only, public): latest probe + ~1h of history."""
return _CDN
"""CDN + origin health (read-only, public). Top-level fields are the CDN
probe (existing shape); .origin is the anidb.app pages/API probe."""
return {**_CDN, "origin": _ORIGIN}
# ---------- auth: multi-user, signed cookies ----------
users.migrate_legacy() # file-based legacy import; db.init() absorbs its output
@@ -621,4 +660,15 @@ def stream(token: str):
return Response(body, media_type=ct, headers=_STREAM_CACHE_HDRS)
@app.get("/", include_in_schema=False)
@app.get("/index.html", include_in_schema=False)
def index():
# index.html is served (not mounted) so ?v={{VERSION}} becomes the git
# SHA at startup; other statics go through the mount below
return HTMLResponse(_INDEX_HTML.replace("{{VERSION}}", VERSION))
_INDEX_HTML = (ROOT / "static" / "index.html").read_text()
app.mount("/", StaticFiles(directory=ROOT / "static", html=True), name="static")