Replaces the hand-rolled inline-SVG topology with a PixiJS 8 scene graph. Why: the old renderer rebuilt all 37 nodes / 36 edges as one innerHTML string every 10s, which tore down and recreated every DOM node. That restarted CSS animations mid-flight and made dragging fight the browser's own hit-testing. The scene graph gives per-node transforms, so pan/zoom is a single container transform instead of getScreenCTM() matrix math. Changes: - static/topo_pixi.js: new self-contained renderer. Owns its Application and tears it down on tab exit so a second WebGL context cannot leak. - static/index.html: the <svg id=topoSvg> host becomes a <div id=topoHost>; the 188-line SVG renderer is replaced by a bridge to the module. - static/vendor/pixi.mjs: PixiJS 8.21.0 self-hosted (MIT). The .mjs build is required; the .js build exports no global. See vendor/README.md. - main.py: mount /static. Pages were served as inline HTMLResponse, so the directory was never mounted and the module had no URL to load from. Two real bugs found by measuring pixels rather than trusting init(): - preserveDrawingBuffer: without it WebGL clears the back buffer after compositing, so any readback or screenshot of the canvas is a coin flip depending on which frame it lands on. The graph rendered intermittently blank. Now enabled: cheap for a 2D scene, and it makes the view capturable. - Layout was centred on the SCROLLABLE width (nodes.length * 130), not the viewport, so with 37 nodes every team and challenge node landed at x=2230-2650 on a 1310px canvas: entirely off-screen. Layout now centres on the visible width and reset() frames the whole graph to fit. test_topo_pixels.js documents three wrong test designs it replaces, all of which reported false failures against a working graph: counting scene-graph children (passes on a blank canvas), diffing against the background colour (the theme is dark by design, so a perfect render measures ~0%), and diffing two Playwright screenshots (both can be captured after the scene was mutated). The check now reads the GL back buffer via readPixels in one evaluate. Verified: 37 nodes / 36 edges drawn (7.94% of frame, max channel delta 225), graph bbox [437,46,881,476] inside the 1310x520 canvas, glGetError=0, no page errors, zoom and frame-to-fit reset working. Platform unregressed: SLA 32/32.
94 lines
3.5 KiB
Python
94 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Live health check for the attack-defense platform after the PixiJS work.
|
|
|
|
Uses the panel's own API with credentials read from .env, so no shell quoting
|
|
hazard and no password on a command line.
|
|
"""
|
|
import json
|
|
import subprocess
|
|
import urllib.request
|
|
import http.cookiejar
|
|
from pathlib import Path
|
|
|
|
BASE = "http://127.0.0.1:18081"
|
|
ENV = Path("/opt/gemastik18-final/panel/.env")
|
|
|
|
|
|
def load_env():
|
|
cfg = {}
|
|
for line in ENV.read_text().splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
k, _, v = line.partition("=")
|
|
cfg[k.strip()] = v.strip().strip('"').strip("'")
|
|
return cfg
|
|
|
|
|
|
def get(url, cookie=None):
|
|
req = urllib.request.Request(url)
|
|
if cookie:
|
|
req.add_header("Cookie", "; ".join(f"{c.name}={c.value}" for c in cookie))
|
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|
return r.read().decode()
|
|
|
|
|
|
def post_json(url, payload, cookie=None):
|
|
data = json.dumps(payload).encode()
|
|
req = urllib.request.Request(url, data=data, method="POST",
|
|
headers={"Content-Type": "application/json"})
|
|
if cookie:
|
|
req.add_header("Cookie", "; ".join(f"{c.name}={c.value}" for c in cookie))
|
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|
return r.read().decode()
|
|
|
|
|
|
def main():
|
|
cfg = load_env()
|
|
jar = http.cookiejar.CookieJar()
|
|
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
|
|
|
|
# main.py's /api/login expects {"user","pass"} — not username/password.
|
|
body = json.dumps({"user": cfg["PANEL_ADMIN_USER"],
|
|
"pass": cfg["PANEL_ADMIN_PASS"]}).encode()
|
|
req = urllib.request.Request(BASE + "/api/login", data=body, method="POST",
|
|
headers={"Content-Type": "application/json"})
|
|
with opener.open(req, timeout=30) as r:
|
|
login = json.loads(r.read().decode())
|
|
print("login:", login)
|
|
|
|
cookie = jar
|
|
teams_raw = json.loads(get(BASE + "/api/teams", cookie))
|
|
teams = teams_raw.get("teams", teams_raw) if isinstance(teams_raw, dict) else teams_raw
|
|
print("\nteams:")
|
|
for t in teams:
|
|
print(f" #{t.get('index')} {t.get('label')} domain={t.get('domain')} "
|
|
f"receivers={t.get('receiver_port')} challenges={len(t.get('challenges') or [])}")
|
|
|
|
topo = json.loads(get(BASE + "/api/topology", cookie))
|
|
print("\ntopology API:",
|
|
{k: (len(v) if isinstance(v, list) else v) for k, v in topo.items()})
|
|
|
|
containers = subprocess.run(
|
|
["docker", "ps", "--format", "{{.Names}}"],
|
|
capture_output=True, text=True, timeout=60).stdout.split()
|
|
team_ct = [c for c in containers if c.endswith(tuple(f"_team{i}" for i in range(1, 10)))]
|
|
orphans = [c for c in containers
|
|
if "_container" in c and not any(c.endswith(f"_team{i}") for i in range(1, 10))]
|
|
print(f"\ncontainers: {len(team_ct)} team-scoped, orphans={orphans}")
|
|
|
|
services = subprocess.run(
|
|
["systemctl", "is-active",
|
|
"gemastik-panel", "gemastik-receiver-service",
|
|
"gemastik-receiver-team1", "gemastik-receiver-team2"],
|
|
capture_output=True, text=True, timeout=60).stdout.strip()
|
|
print("services:\n ", services.replace("\n", "\n "))
|
|
|
|
load = subprocess.run(["uptime"], capture_output=True, text=True).stdout.strip()
|
|
print("load:", load)
|
|
print("expected team containers:", len(teams) * 16, "| actual:", len(team_ct))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|