feat: SLA dashboard per team + points system (100/flag, +50 SLA bonus) + badges juara/runner-up/3rd + scoreboard API public+admin

Panel: /api/scoreboard + /api/public/scoreboard; teams.py: points.json ledger, probe_team_sla_fast, sla_status_all w/ background refresher; team.html: SLA & Skor tab; index.html: admin SLA tab; leaderboard shows points; Phew checker timeouts raised for slow Paillier keygen
This commit is contained in:
2026-09-24 00:49:05 +08:00
parent 3ef905b3bb
commit c35b23a37f
4 changed files with 342 additions and 6 deletions
+39 -3
View File
@@ -25,6 +25,15 @@ BASE_DIR = Path(__file__).parent
app = FastAPI(title="Gemastik A/D Panel")
@app.on_event("startup")
async def _start_background():
"""Warm the SLA scoreboard cache in the background."""
import threading
threading.Thread(target=orch._build_scoreboard, daemon=True,
name="sla-warmup").start()
orch.start_sla_refresher()
CHALLENGES = [
{"name": "blogpost", "port": 10000, "ssh": 10022, "category": "web", "desc": "Flask blog with exiftool + SSTI"},
{"name": "carbeat", "port": 11000, "ssh": 11022, "category": "pwn", "desc": "Binary exploitation menu"},
@@ -276,7 +285,14 @@ async def api_team_status(idx: int, req: Request):
ok = False
results.append({"name": name, "port": st["ports"][name]["chall"],
"ssh": st["ports"][name]["ssh"], "alive": ok})
return {"results": results}
# award SLA bonus when all services are UP (throttled, see add_sla_bonus)
alive = sum(1 for r in results if r["alive"])
bonus = orch.add_sla_bonus(idx, alive, len(results))
return {"results": results, "sla": {"alive": alive, "total": len(results),
"pct": round(100 * alive / max(len(results), 1), 1),
"points": orch.get_team_points(idx),
"rank": orch.team_rank(idx), "badge": orch.team_badge(idx),
"bonus": bonus}}
@app.get("/api/team/{idx}/activity")
async def api_team_activity(idx: int, req: Request):
@@ -630,11 +646,31 @@ async def api_leaderboard(req: Request):
teams = {}
for e in lb["solves"]:
name = team_name(e["team"])
t = teams.setdefault(e["team"], {"team": e["team"], "name": name, "solves": 0, "challs": []})
t = teams.setdefault(e["team"], {"team": e["team"], "name": name, "solves": 0, "challs": [], "points": 0})
t["name"] = name
t["solves"] += 1
t["challs"].append(e["challenge"])
return {"solves": lb["solves"], "teams": sorted(teams.values(), key=lambda x: -x["solves"])}
# attach live points from points.json
for tid, t in teams.items():
t["points"] = orch.get_team_points(tid)
return {"solves": lb["solves"], "teams": sorted(teams.values(), key=lambda x: (-x["points"], -x["solves"]))}
@app.get("/api/scoreboard")
async def api_scoreboard(req: Request):
"""Admin + team: full scoreboard with points, SLA, rank, badges."""
require_login(req)
return orch.sla_status_all()
@app.get("/api/public/scoreboard")
async def api_public_scoreboard():
"""Public scoreboard (no login) — used by the team portal status tab."""
d = orch.sla_status_all()
# strip receiver creds / ports internals for the public view
for t in d["teams"]:
t.pop("domain", None)
return d
@app.get("/api/public/teams")