status: consolidate the pill to one dot + one word; upgrade the panel
- pill: two host dots -> ONE consolidated state (down > slow > up) — the at-a-glance question is 'can I watch?', which is one state; per-host detail belongs in the tap panel - panel: dashed slow-threshold line in the CDN sparkline (drawn at the server's real threshold, not a client copy) + origin up/down step sparkline so 'how long has the origin been down' is visible - server: origin probe keeps a 20-sample hist like the CDN probe; DEGRADED threshold is now ANIWEB_CDN_KBPS (default 40) and is returned in /api/cdn so the UI draws the line at the live value - tests: pill assertions switched to consolidated states; fixtures carry origin hist + threshold; assert both sparklines + threshold line - docs: README + SPEC synced; preview on :8322 restarted (pid 44382)
This commit is contained in:
1 parent
778bf2bfc1
commit
5f7aa010f1
8 files changed
+105
-56
No files matched your search
@@ -7,3 +7,4 @@ server.log
|
|||||||
server.pid
|
server.pid
|
||||||
pi-session-*.html
|
pi-session-*.html
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
server-*.log
|
||||||
@@ -61,9 +61,11 @@ proxy is the way in. See [SPEC.md](SPEC.md) for the rest.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
- **Status** — the header pill shows upstream/CDN health (dots +
|
- **Status** — the header pill shows one consolidated state — up /
|
||||||
label); tap opens a panel with detail + the ~1h throughput trend.
|
slow (KB/s) / down — as a single dot; tap opens a panel with the
|
||||||
A failure overlay names the CDN when it's the likely culprit.
|
per-host breakdown, the ~1h CDN trend (dashed slow-threshold line),
|
||||||
|
and origin up/down history. A failure overlay names the CDN when
|
||||||
|
it's the likely culprit.
|
||||||
|
|
||||||
- **Stats** — `?stats`: watch minutes, episodes, streaks, sub/dub
|
- **Stats** — `?stats`: watch minutes, episodes, streaks, sub/dub
|
||||||
split, weekday/per-day heatmaps, top-10 shows, top searches
|
split, weekday/per-day heatmaps, top-10 shows, top searches
|
||||||
|
|||||||
@@ -82,9 +82,11 @@ an exception falls through to the next provider, a `None` from
|
|||||||
position-event stream.
|
position-event stream.
|
||||||
- `GET /api/cdn` → CDN + origin health (background 64KB probe every
|
- `GET /api/cdn` → CDN + origin health (background 64KB probe every
|
||||||
3 min, ~1h history) — check whether "playback broken" is the CDN.
|
3 min, ~1h history) — check whether "playback broken" is the CDN.
|
||||||
The header status pill is the single UI surface for this (tap opens
|
`DEGRADED` = below `ANIWEB_CDN_KBPS` (default 40). The header pill is
|
||||||
a panel: origin/CDN state, KB/s, sparkline of `hist`); the playback
|
the single UI surface: one consolidated state (down > slow > up, one
|
||||||
failure overlay appends CDN context when degraded/down.
|
dot), tap opens the panel (per-host state + KB/s, CDN sparkline with
|
||||||
|
the dashed threshold line, origin up/down sparkline of `origin.hist`);
|
||||||
|
the playback failure overlay appends CDN context when degraded/down.
|
||||||
- `GET /stream/{token}` → proxied HLS. Token = base64url of the
|
- `GET /stream/{token}` → proxied HLS. Token = base64url of the
|
||||||
upstream URL (host-allowlisted against the providers' self-registered
|
upstream URL (host-allowlisted against the providers' self-registered
|
||||||
CDN hosts — anti-SSRF). Rewrites every non-comment line of m3u8
|
CDN hosts — anti-SSRF). Rewrites every non-comment line of m3u8
|
||||||
|
|||||||
+8
-3
@@ -48,7 +48,10 @@ VERSION = _version()
|
|||||||
_CDN = {"state": "unknown", "kbps": 0.0, "ts": 0.0, "hist": []}
|
_CDN = {"state": "unknown", "kbps": 0.0, "ts": 0.0, "hist": []}
|
||||||
# origin (primary provider pages/API) health — separate host, separate failure
|
# origin (primary provider pages/API) health — separate host, separate failure
|
||||||
# mode (observed 2026-09-03: CDN fine while origin 503'd "Under Maintenance")
|
# mode (observed 2026-09-03: CDN fine while origin 503'd "Under Maintenance")
|
||||||
_ORIGIN = {"state": "unknown", "msg": "", "ts": 0.0}
|
_ORIGIN = {"state": "unknown", "msg": "", "ts": 0.0, "hist": []}
|
||||||
|
# ponytail: fixed threshold; if the CDN's normal throughput drifts below it
|
||||||
|
# the pill sits on "slow" — tune via ANIWEB_CDN_KBPS, not a percentile.
|
||||||
|
_CDN_THRESHOLD = float(os.environ.get("ANIWEB_CDN_KBPS", "40"))
|
||||||
|
|
||||||
|
|
||||||
def _cdn_probe():
|
def _cdn_probe():
|
||||||
@@ -81,7 +84,7 @@ def _cdn_probe():
|
|||||||
kbps = len(r.content) / 1024 / max(time.time() - t, 0.01)
|
kbps = len(r.content) / 1024 / max(time.time() - t, 0.01)
|
||||||
except Exception:
|
except Exception:
|
||||||
kbps = 0.0
|
kbps = 0.0
|
||||||
now = "OK" if kbps >= 40 else "DEGRADED"
|
now = "OK" if kbps >= _CDN_THRESHOLD else "DEGRADED"
|
||||||
if now != state:
|
if now != state:
|
||||||
print(f"[probe] CDN {now} ({kbps:.0f} KB/s)", flush=True)
|
print(f"[probe] CDN {now} ({kbps:.0f} KB/s)", flush=True)
|
||||||
state = now
|
state = now
|
||||||
@@ -103,6 +106,8 @@ def _origin_probe():
|
|||||||
state, msg = "DOWN", str(e)
|
state, msg = "DOWN", str(e)
|
||||||
prev = _ORIGIN["state"]
|
prev = _ORIGIN["state"]
|
||||||
_ORIGIN.update(state=state, msg=msg, ts=time.time())
|
_ORIGIN.update(state=state, msg=msg, ts=time.time())
|
||||||
|
_ORIGIN["hist"].append({"t": time.time(), "state": state})
|
||||||
|
del _ORIGIN["hist"][:-20]
|
||||||
if state != prev:
|
if state != prev:
|
||||||
print(f"[probe] ORIGIN {state} ({msg})", flush=True)
|
print(f"[probe] ORIGIN {state} ({msg})", flush=True)
|
||||||
|
|
||||||
@@ -114,7 +119,7 @@ threading.Thread(target=_cdn_probe, daemon=True).start()
|
|||||||
async def cdn_status():
|
async def cdn_status():
|
||||||
"""CDN + origin health (read-only, public). Top-level fields are the CDN
|
"""CDN + origin health (read-only, public). Top-level fields are the CDN
|
||||||
probe (existing shape); .origin is the anidb.app pages/API probe."""
|
probe (existing shape); .origin is the anidb.app pages/API probe."""
|
||||||
return {**_CDN, "origin": _ORIGIN}
|
return {**_CDN, "threshold": _CDN_THRESHOLD, "origin": _ORIGIN}
|
||||||
|
|
||||||
# ---------- auth: multi-user, signed cookies ----------
|
# ---------- auth: multi-user, signed cookies ----------
|
||||||
users.migrate_legacy() # file-based legacy import; db.init() absorbs its output
|
users.migrate_legacy() # file-based legacy import; db.init() absorbs its output
|
||||||
|
|||||||
+55
-30
@@ -940,25 +940,27 @@ function statusPill() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderPill(d) {
|
function renderPill(d) {
|
||||||
|
// ONE dot + one word: the at-a-glance question is "can I watch?", which is
|
||||||
|
// one state — down (origin dead) > slow (CDN degraded) > up. Per-host
|
||||||
|
// detail belongs in the tap panel, not here.
|
||||||
const p = document.getElementById("pill");
|
const p = document.getElementById("pill");
|
||||||
if (!p) return;
|
if (!p) return;
|
||||||
const o = d.origin.state === "OK" ? "ok" : d.origin.state === "DOWN" ? "down" : "";
|
const known = (d.origin.state === "OK" || d.origin.state === "DOWN")
|
||||||
const c = d.state === "OK" ? "ok" : d.state === "DEGRADED" ? "degraded" : "";
|
&& (d.state === "OK" || d.state === "DEGRADED");
|
||||||
p.className = "pill" + (o === "down" ? " odown" : "");
|
const down = d.origin.state === "DOWN";
|
||||||
p.querySelector(".o").className = "o " + o;
|
const slow = !down && d.state === "DEGRADED";
|
||||||
p.querySelector(".c").className = "c " + c;
|
const st = !known ? "" : down ? "down" : slow ? "slow" : "ok";
|
||||||
|
p.className = "pill" + (st === "down" ? " down" : "");
|
||||||
|
p.querySelector(".d").className = "d" + (st ? " " + st : "");
|
||||||
p.querySelector("span").textContent =
|
p.querySelector("span").textContent =
|
||||||
o === "down" ? "upstream down"
|
!st ? "…" : st === "down" ? "down"
|
||||||
: c === "degraded" ? "cdn " + d.kbps + " KB/s"
|
: st === "slow" ? "slow · " + d.kbps + " KB/s" : "up";
|
||||||
: "up";
|
p.title = "upstream site: " + d.origin.state + (d.origin.msg ? " — " + d.origin.msg : "")
|
||||||
// per-dot hover tooltips (desktop) + full aria label (a11y / screen readers)
|
+ " · video CDN: " + d.state + " — " + d.kbps + " KB/s";
|
||||||
p.querySelector(".o").title = "upstream site (primary provider pages/API): " + d.origin.state
|
p.setAttribute("aria-label", "status: " + (!st ? "checking"
|
||||||
+ (d.origin.msg ? " — " + d.origin.msg : "");
|
: st === "down" ? "down — upstream " + d.origin.state
|
||||||
p.querySelector(".c").title = "video CDN throughput: " + d.state
|
: st === "slow" ? "slow — CDN " + d.state + ", " + d.kbps + " KB/s"
|
||||||
+ " — " + d.kbps + " KB/s";
|
: "up"));
|
||||||
p.setAttribute("aria-label", "upstream site " + d.origin.state
|
|
||||||
+ (d.origin.msg ? ": " + d.origin.msg : "")
|
|
||||||
+ "; CDN " + d.state + ", " + d.kbps + " KB/s");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleStatusPanel() {
|
function toggleStatusPanel() {
|
||||||
@@ -978,31 +980,54 @@ function renderStatusPanel(d) {
|
|||||||
w.append(el("div", "sp-row", "upstream: " + d.origin.state
|
w.append(el("div", "sp-row", "upstream: " + d.origin.state
|
||||||
+ (d.origin.msg ? " (" + d.origin.msg + ")" : "")));
|
+ (d.origin.msg ? " (" + d.origin.msg + ")" : "")));
|
||||||
w.append(el("div", "sp-row", "CDN: " + d.state + " — " + d.kbps + " KB/s"));
|
w.append(el("div", "sp-row", "CDN: " + d.state + " — " + d.kbps + " KB/s"));
|
||||||
const h = d.hist || [];
|
const ch = d.hist || [];
|
||||||
if (h.length > 1) {
|
if (ch.length > 1) {
|
||||||
w.append(sparkline(h));
|
w.append(sparkline(ch.map(s => s.kbps), d.threshold));
|
||||||
w.append(el("div", "sp-cap", "last " + Math.round((h[h.length - 1].t - h[0].t) / 60) + " min"));
|
w.append(el("div", "sp-cap", "CDN, last " + _mins(ch)
|
||||||
|
+ " min (dashed = slow threshold " + Math.round(d.threshold) + " KB/s)"));
|
||||||
|
}
|
||||||
|
const oh = d.origin.hist || [];
|
||||||
|
if (oh.length > 1) {
|
||||||
|
const up = oh[oh.length - 1].state === "OK";
|
||||||
|
w.append(sparkline(oh.map(s => (s.state === "OK" ? 1 : 0)), null, true, up));
|
||||||
|
w.append(el("div", "sp-cap", "upstream, last " + _mins(oh) + " min (flat bottom = down)"));
|
||||||
}
|
}
|
||||||
const age = Math.max(0, Math.round((Date.now() / 1000 - d.ts) / 60));
|
const age = Math.max(0, Math.round((Date.now() / 1000 - d.ts) / 60));
|
||||||
w.append(el("div", "sp-cap", "updated " + age + " min ago"));
|
w.append(el("div", "sp-cap", "updated " + age + " min ago"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function sparkline(hist) {
|
function _mins(h) { return Math.round((h[h.length - 1].t - h[0].t) / 60); }
|
||||||
// ~20 samples (3-min spacing) -> one inline-SVG polyline, scaled to the
|
|
||||||
// window max. No lib: it's a single polyline.
|
function sparkline(values, threshold, binary = false, healthy = true) {
|
||||||
|
// ~20 samples (3-min spacing) -> one inline-SVG polyline, scaled so a
|
||||||
|
// dashed threshold line (when given) always fits. binary = up(1)/down(0)
|
||||||
|
// step line for the origin probe. No lib: it's a single polyline.
|
||||||
const W = 120, H = 28, PAD = 2;
|
const W = 120, H = 28, PAD = 2;
|
||||||
const max = Math.max(...hist.map(s => s.kbps), 1);
|
const max = Math.max(Math.max(...values), threshold || 0, binary ? 1 : 0) || 1;
|
||||||
const pts = hist.map((s, i) =>
|
|
||||||
(i / (hist.length - 1) * W).toFixed(1) + ","
|
|
||||||
+ (H - PAD - Math.min(s.kbps / max, 1) * (H - 2 * PAD)).toFixed(1)).join(" ");
|
|
||||||
const NS = "http://www.w3.org/2000/svg";
|
const NS = "http://www.w3.org/2000/svg";
|
||||||
const svg = document.createElementNS(NS, "svg");
|
const svg = document.createElementNS(NS, "svg");
|
||||||
svg.setAttribute("viewBox", "0 0 " + W + " " + H);
|
svg.setAttribute("viewBox", "0 0 " + W + " " + H);
|
||||||
svg.setAttribute("class", "sp-svg");
|
svg.setAttribute("class", "sp-svg" + (binary && !healthy ? " bad" : ""));
|
||||||
svg.setAttribute("role", "img");
|
svg.setAttribute("role", "img");
|
||||||
svg.setAttribute("aria-label", "CDN throughput trend, last hour");
|
svg.setAttribute("aria-label", binary ? "upstream up/down, last hour"
|
||||||
|
: "CDN throughput trend, last hour");
|
||||||
|
if (threshold) {
|
||||||
|
const ty = Math.max(PAD, H - PAD - Math.min(threshold / max, 1) * (H - 2 * PAD));
|
||||||
|
const l = document.createElementNS(NS, "line");
|
||||||
|
l.setAttribute("x1", 0); l.setAttribute("x2", W);
|
||||||
|
l.setAttribute("y1", ty.toFixed(1)); l.setAttribute("y2", ty.toFixed(1));
|
||||||
|
l.setAttribute("class", "sp-thr");
|
||||||
|
svg.append(l);
|
||||||
|
}
|
||||||
|
const pts = [];
|
||||||
|
values.forEach((v, i) => {
|
||||||
|
const x0 = (i / (values.length - 1)) * W;
|
||||||
|
const y = H - PAD - Math.min(v / max, 1) * (H - 2 * PAD);
|
||||||
|
pts.push(x0.toFixed(1) + "," + y.toFixed(1));
|
||||||
|
if (binary) pts.push(((i + 1) / (values.length - 1) * W).toFixed(1) + "," + y.toFixed(1));
|
||||||
|
});
|
||||||
const pl = document.createElementNS(NS, "polyline");
|
const pl = document.createElementNS(NS, "polyline");
|
||||||
pl.setAttribute("points", pts);
|
pl.setAttribute("points", pts.join(" "));
|
||||||
svg.append(pl);
|
svg.append(pl);
|
||||||
return svg;
|
return svg;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
<form onsubmit="const q=document.getElementById('q').value.trim(); if(q) location='?q='+encodeURIComponent(q); return false">
|
<form onsubmit="const q=document.getElementById('q').value.trim(); if(q) location='?q='+encodeURIComponent(q); return false">
|
||||||
<input id="q" placeholder="search anime…" aria-label="search anime" autocomplete="off" autocapitalize="off" spellcheck="false">
|
<input id="q" placeholder="search anime…" aria-label="search anime" autocomplete="off" autocapitalize="off" spellcheck="false">
|
||||||
</form>
|
</form>
|
||||||
<span id="pill" class="pill"><i class="o"></i><i class="c"></i><span></span></span>
|
<span id="pill" class="pill"><i class="d"></i><span></span></span>
|
||||||
<span id="me" class="muted"></span>
|
<span id="me" class="muted"></span>
|
||||||
<div id="menubox" style="display:none">
|
<div id="menubox" style="display:none">
|
||||||
<button id="menu" class="tiny" aria-label="menu" aria-haspopup="true">☰</button>
|
<button id="menu" class="tiny" aria-label="menu" aria-haspopup="true">☰</button>
|
||||||
|
|||||||
+7
-5
@@ -176,12 +176,14 @@ header .pill { display: flex; align-items: center; gap: .4rem; flex: none;
|
|||||||
.sp-row { font-variant-numeric: tabular-nums; }
|
.sp-row { font-variant-numeric: tabular-nums; }
|
||||||
.sp-cap { color: var(--muted); font-size: .68rem; }
|
.sp-cap { color: var(--muted); font-size: .68rem; }
|
||||||
.sp-svg { width: 100%; height: 28px; color: var(--acc); }
|
.sp-svg { width: 100%; height: 28px; color: var(--acc); }
|
||||||
|
.sp-svg.bad { color: #ef4444; } /* origin currently down */
|
||||||
.sp-svg polyline { fill: none; stroke: currentColor; stroke-width: 1.5; }
|
.sp-svg polyline { fill: none; stroke: currentColor; stroke-width: 1.5; }
|
||||||
header .pill i { width: 8px; height: 8px; flex: none; border-radius: 50%; background: #6b7280; }
|
.sp-thr { stroke: #f59e0b; stroke-width: 1; stroke-dasharray: 3 3; }
|
||||||
header .pill .o.ok, header .pill .c.ok { background: #22c55e; }
|
header .pill .d { width: 8px; height: 8px; flex: none; border-radius: 50%; background: #6b7280; }
|
||||||
header .pill .c.degraded { background: #f59e0b; }
|
header .pill .d.ok { background: #22c55e; }
|
||||||
header .pill .o.down { background: #ef4444; }
|
header .pill .d.slow { background: #f59e0b; }
|
||||||
header .pill.odown { border-color: #7f1d1d; }
|
header .pill .d.down { background: #ef4444; }
|
||||||
|
header .pill.down { border-color: #7f1d1d; }
|
||||||
#toasts { position: fixed; bottom: 1rem; left: 50%; transform: translateX(-50%);
|
#toasts { position: fixed; bottom: 1rem; left: 50%; transform: translateX(-50%);
|
||||||
display: flex; flex-direction: column; gap: .4rem; z-index: 9; }
|
display: flex; flex-direction: column; gap: .4rem; z-index: 9; }
|
||||||
@keyframes toast-in { from { opacity: 0; transform: translateY(.4rem); } }
|
@keyframes toast-in { from { opacity: 0; transform: translateY(.4rem); } }
|
||||||
|
|||||||
+23
-11
@@ -4,6 +4,9 @@
|
|||||||
runs against LIVE upstream, so its real data must never leak into these
|
runs against LIVE upstream, so its real data must never leak into these
|
||||||
assertions. Everything else (login, home) hits the throwaway server, which
|
assertions. Everything else (login, home) hits the throwaway server, which
|
||||||
never touches the real data. Skips if playwright is absent.
|
never touches the real data. Skips if playwright is absent.
|
||||||
|
|
||||||
|
Pill states are CONSOLIDATED (one dot + one word): down (origin dead)
|
||||||
|
> slow (CDN degraded) > up. Per-host detail lives in the tap panel.
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
@@ -18,10 +21,14 @@ POLL = 100
|
|||||||
|
|
||||||
|
|
||||||
def _cdn(state, kbps, origin="OK"):
|
def _cdn(state, kbps, origin="OK"):
|
||||||
|
# origin hist: all OK except the last 3 when origin is DOWN
|
||||||
|
ohist = [{"t": 1750000000 - 180 * (10 - i),
|
||||||
|
"state": "DOWN" if (origin == "DOWN" and i >= 8) else "OK"}
|
||||||
|
for i in range(11)]
|
||||||
return {
|
return {
|
||||||
"state": state, "kbps": kbps, "ts": 1750000000,
|
"state": state, "kbps": kbps, "ts": 1750000000, "threshold": 40,
|
||||||
"hist": [{"t": 1750000000 - 180 * (10 - i), "kbps": 20 + i * 10} for i in range(11)],
|
"hist": [{"t": 1750000000 - 180 * (10 - i), "kbps": 20 + i * 10} for i in range(11)],
|
||||||
"origin": {"state": origin, "msg": "http 200", "ts": 1750000000},
|
"origin": {"state": origin, "msg": "http 200", "ts": 1750000000, "hist": ohist},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -57,13 +64,15 @@ async def _pill_ok():
|
|||||||
browser, page = await _open(pw, _cdn("OK", 120))
|
browser, page = await _open(pw, _cdn("OK", 120))
|
||||||
try:
|
try:
|
||||||
await page.wait_for_function(
|
await page.wait_for_function(
|
||||||
"document.querySelector('#pill .c').className.includes('ok')",
|
"document.querySelector('#pill .d').className.includes('ok')",
|
||||||
timeout=5000, polling=POLL)
|
timeout=5000, polling=POLL)
|
||||||
assert await page.text_content("#pill span") == "up"
|
assert await page.text_content("#pill span") == "up"
|
||||||
# tap opens the panel: detail rows + sparkline with one point
|
# tap opens the panel: two sparklines (CDN + origin), the CDN
|
||||||
# per hist sample
|
# one carries the dashed threshold line and one point per sample
|
||||||
await _js_click(page, "#pill")
|
await _js_click(page, "#pill")
|
||||||
await page.wait_for_selector("#status-panel .sp-svg polyline")
|
await page.wait_for_selector("#status-panel .sp-svg polyline")
|
||||||
|
assert await page.locator("#status-panel polyline").count() == 2
|
||||||
|
assert await page.locator("#status-panel .sp-thr").count() == 1
|
||||||
pts = await page.get_attribute("#status-panel .sp-svg polyline", "points")
|
pts = await page.get_attribute("#status-panel .sp-svg polyline", "points")
|
||||||
assert len(pts.split()) == 11
|
assert len(pts.split()) == 11
|
||||||
assert "CDN: OK — 120 KB/s" in await page.text_content("#status-panel")
|
assert "CDN: OK — 120 KB/s" in await page.text_content("#status-panel")
|
||||||
@@ -87,9 +96,9 @@ async def _pill_degraded():
|
|||||||
browser, page = await _open(pw, _cdn("DEGRADED", 23))
|
browser, page = await _open(pw, _cdn("DEGRADED", 23))
|
||||||
try:
|
try:
|
||||||
await page.wait_for_function(
|
await page.wait_for_function(
|
||||||
"document.querySelector('#pill .c').className.includes('degraded')",
|
"document.querySelector('#pill .d').className.includes('slow')",
|
||||||
timeout=5000, polling=POLL)
|
timeout=5000, polling=POLL)
|
||||||
assert await page.text_content("#pill span") == "cdn 23 KB/s"
|
assert await page.text_content("#pill span") == "slow · 23 KB/s"
|
||||||
await _js_click(page, "#pill")
|
await _js_click(page, "#pill")
|
||||||
await page.wait_for_selector("#status-panel")
|
await page.wait_for_selector("#status-panel")
|
||||||
assert "CDN: DEGRADED — 23 KB/s" in await page.text_content("#status-panel")
|
assert "CDN: DEGRADED — 23 KB/s" in await page.text_content("#status-panel")
|
||||||
@@ -105,11 +114,14 @@ async def _pill_origin_down():
|
|||||||
async with async_playwright() as pw:
|
async with async_playwright() as pw:
|
||||||
browser, page = await _open(pw, _cdn("OK", 120, origin="DOWN"))
|
browser, page = await _open(pw, _cdn("OK", 120, origin="DOWN"))
|
||||||
try:
|
try:
|
||||||
|
# down beats a healthy CDN: one consolidated state
|
||||||
await page.wait_for_function(
|
await page.wait_for_function(
|
||||||
"document.querySelector('#pill .o').className.includes('down')",
|
"document.querySelector('#pill .d').className.includes('down')",
|
||||||
timeout=5000, polling=POLL)
|
timeout=5000, polling=POLL)
|
||||||
assert await page.text_content("#pill span") == "upstream down"
|
assert await page.text_content("#pill span") == "down"
|
||||||
cls = await page.get_attribute("#pill", "class")
|
assert "down" in await page.get_attribute("#pill", "class")
|
||||||
assert "odown" in cls
|
await _js_click(page, "#pill")
|
||||||
|
await page.wait_for_selector("#status-panel .sp-svg.bad")
|
||||||
|
assert "upstream: DOWN" in await page.text_content("#status-panel")
|
||||||
finally:
|
finally:
|
||||||
await browser.close()
|
await browser.close()
|
||||||
Reference in new issue
Block a user