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:
1 parent
3f715434f0
commit
ff8311c1a8
4 files changed
+178
-59
No files matched your search
+53
-3
@@ -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")
|
||||
Reference in new issue
Block a user