draggable topology + attack visualizer + tools in containers

- topology nodes draggable (pointer events, SVG transform), layout hint shown
- attack visualizer: /api/attacks logs attacker->target events; red pulsing
  dashed arcs on recent attacks (60s hot), ⚔ counts ok/fail
- submit_flag now takes attacker_idx vs target_idx (A/D semantics); UI has
  target dropdown (enemy teams), leaderboard records target
- containers get vim+curl+wget+netcat+git+pip3 (Dockerfiles blogpost/cdn/
  phew/sheesh/warmup); warmup base ubuntu:20.04 EOL -> 24.04
- team portal: target dropdown refreshed after login (was empty pre-auth)
This commit is contained in:
root
2026-09-23 18:05:44 +08:00
parent 44dc1ac852
commit 24dbf0a662
9 changed files with 196 additions and 23 deletions
+30 -1
View File
@@ -138,7 +138,11 @@
<option value="sheesh">sheesh (crypto)</option>
<option value="warmup">warmup</option>
</select>
<label>Flag</label>
<label>Target (tim musuh yang diserang)</label>
<select id="target">
<option value="">Memuat target…</option>
</select>
<label>Flag (yang kamu curi dari target)</label>
<input type="text" id="flag" placeholder="GEMASTIK18{...}" autocomplete="off">
<button onclick="submitFlag()" style="width:100%;margin-top:16px">Submit Flag</button>
<div id="result"></div>
@@ -229,6 +233,8 @@ async function doLogin() {
document.getElementById('portalScreen').style.display = 'block';
document.getElementById('logoutBtn').style.display = '';
loadInfo();
loadTargetDropdown(); // target dropdown needs auth — refresh after login
loadTargets();
} else {
err.textContent = 'Password salah. Coba lagi.';
err.style.display = 'block';
@@ -305,6 +311,7 @@ async function submitFlag() {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
team: TEAM_ID,
target: parseInt(document.getElementById('target').value || '0', 10) || TEAM_ID,
challenge: document.getElementById('challenge').value,
flag: document.getElementById('flag').value.trim(),
}),
@@ -313,6 +320,28 @@ async function submitFlag() {
else { res.className = 'err'; res.textContent = '❌ ' + (d.error || 'Gagal'); }
}
// target dropdown: all enemy teams (exclude own)
async function loadTargetDropdown() {
try {
const d = await api(`/api/team/${TEAM_ID}/targets`);
const sel = document.getElementById('target');
const targets = (d.targets || []).filter(t => t.team_idx !== TEAM_ID);
const seen = new Map();
for (const t of targets) seen.set(t.team_idx, t.team_label || ('Team ' + t.team_idx));
sel.innerHTML = '';
if (!seen.size) {
sel.innerHTML = '<option value="">Tidak ada tim musuh</option>';
return;
}
for (const [idx, label] of seen) {
const o = document.createElement('option');
o.value = idx; o.textContent = `${label} (tim ${idx})`;
sel.appendChild(o);
}
} catch (e) { /* non-fatal */ }
}
loadTargetDropdown();
checkSession();
</script>
</body>