diff --git a/panel/main.py b/panel/main.py
index c043fea..d47d634 100644
--- a/panel/main.py
+++ b/panel/main.py
@@ -202,7 +202,7 @@ async def api_team_session(idx: int, req: Request):
@app.get("/api/team/{idx}/targets")
async def api_team_targets(idx: int, req: Request):
- """Team targets — requires team login + own host."""
+ """Team targets — requires team login + own host. Only domain + port."""
td = orch.TEAMS_DIR / f"team{idx}"
if not (td / "state.json").exists():
raise HTTPException(404, "Team not found")
@@ -223,12 +223,8 @@ async def api_team_targets(idx: int, req: Request):
if not p:
continue
out.append({
- "team": st.get("label", f"Team {st.get('index')}"),
- "team_idx": st.get("index"),
- "domain": st.get("domain"),
- "challenge": name,
+ "domain": st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".gemastik.imrnes.team"),
"port": p["chall"],
- "ssh": p["ssh"],
})
return {"targets": out}
@@ -510,6 +506,19 @@ async def api_randomize(idx: int, req: Request):
raise HTTPException(500, str(e))
+@app.post("/api/reset/scores")
+async def api_reset_scores(req: Request):
+ """Admin: wipe leaderboard (all solves)."""
+ require_login(req)
+ return orch.reset_scores()
+
+@app.post("/api/reset/environment")
+async def api_reset_environment(req: Request):
+ """Admin: full environment reset (stop all, remove teams/containers/receivers)."""
+ require_login(req)
+ return orch.reset_environment()
+
+
@app.post("/api/flag/submit")
async def api_flag_submit(req: Request):
"""Public endpoint: teams submit flags. No login needed."""
@@ -549,6 +558,29 @@ async def api_public_teams():
"""Public list of team names (for the submit dropdown)."""
return {"teams": [{"index": t["index"], "label": t.get("label", f"Team {t['index']}")} for t in orch.list_teams()]}
+@app.get("/api/targets")
+async def api_admin_targets(req: Request):
+ """Admin: matrix of every team's service domain:port (public targets)."""
+ require_login(req)
+ out = []
+ for d in sorted(orch.TEAMS_DIR.glob("team*")):
+ if not (d / "state.json").exists():
+ continue
+ st = json.loads((d / "state.json").read_text())
+ dom = st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".gemastik.imrnes.team")
+ for name, coff, soff in orch.CHALLENGES:
+ p = st["ports"].get(name)
+ if not p:
+ continue
+ out.append({
+ "team": st.get("index"),
+ "team_label": st.get("label", f"Team {st.get('index')}"),
+ "challenge": name,
+ "domain": dom,
+ "port": p["chall"],
+ })
+ return {"targets": out}
+
# ============ WebSocket SSH terminal (team portal) ============
import paramiko
diff --git a/panel/static/index.html b/panel/static/index.html
index fa72225..a541d7e 100644
--- a/panel/static/index.html
+++ b/panel/static/index.html
@@ -115,6 +115,8 @@
--:--:--
+
+
@@ -125,6 +127,7 @@
+
@@ -190,6 +193,17 @@
+
+
+
+
+ 🎯 Target Matrix
+ Domain:port semua service semua tim — buat dibagikan ke peserta sebagai daftar target
+
+
Memuat…
+
+
+
@@ -519,6 +533,28 @@ async function stopAllTeams() {
finally { hideLoading(); }
}
+async function resetScores() {
+ if (!confirm('⚠️ Apakah anda yakin ingin mereset skor?\n\nSemua poin & solve di leaderboard akan DIHAPUS.\nTindakan ini tidak bisa dibatalkan.')) return;
+ showLoading('Menghapus semua skor…');
+ try {
+ const d = await api('/api/reset/scores', {method:'POST', headers:{'Content-Type':'application/json'}, body: '{}'});
+ toast('Skor direset: semua solve dihapus', false);
+ } catch (e) { toast(e.message, true); }
+ finally { hideLoading(); }
+}
+
+async function resetEnv() {
+ if (!confirm('⚠️ Apakah anda yakin ingin mereset environment?\n\nSEMUA team akan DISTOP:\n• Container challenge dihapus\n• Receiver team dihapus\n• Folder team dihapus (perlu create ulang)\n• Skor leaderboard direset\n\nTindakan ini TIDAK BISA dibatalkan.')) return;
+ showLoading('Reset environment…\nMenghentikan semua team + menghapus container/receiver (beberapa menit)');
+ try {
+ const d = await api('/api/reset/environment', {method:'POST', headers:{'Content-Type':'application/json'}, body: '{}'});
+ const n = (d.stopped || []).length;
+ toast(`Environment direset: ${n} team dihentikan & dihapus`, false);
+ setTimeout(() => location.reload(), 1200);
+ } catch (e) { toast(e.message, true); }
+ finally { hideLoading(); }
+}
+
async function randomizeTeam(idx) {
if (!confirm(`Randomize SEMUA flag untuk Team ${idx}? Container akan di-recreate.`)) return;
try {
@@ -619,6 +655,27 @@ async function loadCreds() {
} catch (e) { toast(e.message, true); }
}
+// ---------- Target Matrix ----------
+async function loadTargetMatrix() {
+ const box = document.getElementById('targetMatrix');
+ try {
+ const d = await api('/api/targets');
+ const targets = d.targets || [];
+ if (!targets.length) { box.textContent = 'Belum ada team.'; return; }
+ // group by team
+ const rows = [];
+ let curTeam = null;
+ for (const t of targets) {
+ if (t.team !== curTeam) {
+ rows.push(`\n[${esc(t.team_label || ('Team ' + t.team))}]`);
+ curTeam = t.team;
+ }
+ rows.push(` ${esc(t.domain)}:${t.port} (${esc(t.challenge)})`);
+ }
+ box.textContent = rows.join('\n');
+ } catch (e) { box.textContent = 'Gagal: ' + e.message; }
+}
+
// ---------- misc ----------
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
async function logout() {
diff --git a/panel/static/team.html b/panel/static/team.html
index 97f384e..bbf345a 100644
--- a/panel/static/team.html
+++ b/panel/static/team.html
@@ -264,10 +264,7 @@ async function loadTargets() {
if (!targets.length) { box.textContent = 'Belum ada tim musuh.'; return; }
let html = '=== TARGET MUSUH ===\n\n';
for (const t of targets) {
- html += `[${esc(t.team)}] — ${esc(t.challenge)}\n`;
- if (t.domain) html += ` domain : https://${esc(t.domain)}\n`;
- html += ` service: ${esc(window.location.hostname)}:${t.port}\n`;
- html += ` ssh : ${esc(window.location.hostname)}:${t.ssh}\n\n`;
+ html += `${esc(t.domain)}:${t.port}\n`;
}
box.textContent = html;
}
diff --git a/panel/teams.py b/panel/teams.py
index 99f12ea..1998edd 100644
--- a/panel/teams.py
+++ b/panel/teams.py
@@ -402,6 +402,50 @@ def submit_flag(team_idx: int, chall: str, flag: str, team_name: str = "") -> di
lb_path.write_text(json.dumps(lb, indent=2))
return {"success": True, "team": team_idx, "challenge": chall}
+def reset_scores() -> dict:
+ """Wipe the leaderboard (all solves removed)."""
+ lb_path = TEAMS_DIR / "leaderboard.json"
+ lb_path.write_text(json.dumps({"solves": []}, indent=2))
+ return {"ok": True, "cleared": True}
+
+def reset_environment() -> dict:
+ """Full env reset: stop all teams, remove containers + receiver services,
+ delete team dirs (fresh for re-create). Keeps panel + images."""
+ results = []
+ for d in sorted(TEAMS_DIR.glob("team*")):
+ sf = d / "state.json"
+ if not sf.exists():
+ continue
+ st = json.loads(sf.read_text())
+ idx = st["index"]
+ try:
+ stop_team(idx) # compose down + stop receiver service + set status
+ except Exception as e:
+ results.append({"team": idx, "ok": False, "err": str(e)})
+ continue
+ # remove the per-team systemd receiver unit
+ subprocess.run(["systemctl", "disable", f"gemastik-receiver-team{idx}.service"],
+ check=False, capture_output=True)
+ subprocess.run(["systemctl", "stop", f"gemastik-receiver-team{idx}.service"],
+ check=False, capture_output=True)
+ unit = Path("/etc/systemd/system") / f"gemastik-receiver-team{idx}.service"
+ if unit.exists():
+ unit.unlink()
+ results.append({"team": idx, "ok": True})
+ subprocess.run(["systemctl", "daemon-reload"], check=False)
+ # remove team dirs entirely (fresh for re-create)
+ for d in sorted(TEAMS_DIR.glob("team*")):
+ shutil.rmtree(d, ignore_errors=True)
+ # wipe leaderboard too
+ reset_scores()
+ # drop team domains from Traefik (regenerate — no teams left)
+ try:
+ ensure_team_domains()
+ except Exception:
+ pass
+ return {"ok": True, "stopped": results, "teams_dir": str(TEAMS_DIR)}
+
+
if __name__ == "__main__":
import sys
cmd = sys.argv[1] if len(sys.argv) > 1 else "list"