feat: target matrix + reset scores/environment buttons

- /api/targets (admin): matrix of all teams' domain:port targets
- /api/reset/scores: wipe leaderboard (admin, confirm dialog)
- /api/reset/environment: stop all teams, remove containers+receivers+
  team dirs+systemd units, wipe scores, drop domains (admin, confirm)
- portal targets now only domain+port (no ssh/labels)
- UI: tab Target Matrix, header buttons Reset Skor / Reset Environment
  with confirm() alerts 'apakah anda yakin ingin mereset...'
This commit is contained in:
root
2026-09-23 17:14:42 +08:00
parent b66530e7fd
commit 44dc1ac852
4 changed files with 140 additions and 10 deletions
+57
View File
@@ -115,6 +115,8 @@
<div class="actions">
<span class="pill" id="clock">--:--:--</span>
<button onclick="refresh()">🔄 Refresh</button>
<button class="danger" onclick="resetScores()" style="background:#dc2626;border:none;border-radius:8px;padding:8px 14px;color:#fff;font-weight:600;cursor:pointer">🧹 Reset Skor</button>
<button class="danger" onclick="resetEnv()" style="background:#7f1d1d;border:none;border-radius:8px;padding:8px 14px;color:#fff;font-weight:600;cursor:pointer">💥 Reset Environment</button>
<button onclick="logout()">Keluar</button>
</div>
</header>
@@ -125,6 +127,7 @@
<button class="tab" data-view="teams" onclick="showView('teams'); loadTeams()">👥 Teams</button>
<button class="tab" data-view="logs" onclick="showView('logs'); loadLogs()">📜 Logs</button>
<button class="tab" data-view="creds" onclick="showView('creds'); loadCreds()">🔑 Creds</button>
<button class="tab" data-view="targets" onclick="showView('targets'); loadTargetMatrix()">🎯 Target Matrix</button>
</div>
<!-- Challenges view -->
@@ -190,6 +193,17 @@
<div class="grid" id="credsGrid"></div>
</div>
<!-- Target Matrix view -->
<div class="view" id="view-targets">
<div class="card">
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<b style="color:#5ad1ff">🎯 Target Matrix</b>
<span style="font-size:11px;color:#718096">Domain:port semua service semua tim — buat dibagikan ke peserta sebagai daftar target</span>
</div>
<div id="targetMatrix" style="margin-top:14px;font-family:ui-monospace,monospace;font-size:12px;line-height:1.9;color:#dbe6f4;background:#0a101f;border:1px solid #1e3a5f;border-radius:10px;padding:14px;overflow-x:auto">Memuat…</div>
</div>
</div>
<div class="footer-note">Receiver: https://gemastik.imrnes.team · Panel: https://panel.gemastik.imrnes.team · Auto-refresh tiap 15 detik</div>
<!-- modal flag -->
@@ -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() {
+1 -4
View File
@@ -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;
}