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
+41
View File
@@ -134,6 +134,7 @@
<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>
<button class="tab" data-view="sla" onclick="showView('sla'); loadSla()">📊 SLA & Skor</button>
</div>
<!-- Challenges view -->
@@ -217,6 +218,17 @@
</div>
</div>
<!-- SLA & Scoreboard view -->
<div class="view" id="view-sla">
<div class="card">
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<b style="color:#5ad1ff">📊 SLA & Skor Tim</b>
<span style="font-size:11px;color:#718096">Status service tiap tim (dicek otomatis tiap ~30 detik) + poin: +100/flag dicuri, +50 bonus SLA saat 6/6 UP. Auto-refresh 30 detik.</span>
</div>
<div id="slaAdminList" 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 -->
@@ -297,6 +309,7 @@ function showView(v) {
document.querySelectorAll('.tab').forEach(b => b.classList.toggle('active', b.dataset.view === v));
document.querySelectorAll('.view').forEach(x => x.classList.toggle('active', x.id === 'view-' + v));
if (v === 'topo') loadTopo(); // refresh attacks immediately on tab switch + every 10s
if (v === 'sla') loadSla();
}
let topoTimer = null;
function startTopoTimer() {
@@ -305,6 +318,34 @@ function startTopoTimer() {
}, 10000);
}
startTopoTimer();
let slaTimer = null;
function startSlaTimer() {
if (!slaTimer) slaTimer = setInterval(() => {
if (document.getElementById('view-sla').classList.contains('active')) loadSla();
}, 30000);
}
startSlaTimer();
async function loadSla() {
const box = document.getElementById('slaAdminList');
if (!box) return;
box.innerHTML = 'Memuat…';
try {
const d = await api('/api/scoreboard');
const rows = (d.teams || []).map(t => {
const pct = Math.round(t.sla_pct || 0);
const color = pct === 100 ? '#2ecc71' : (pct >= 60 ? '#f1c40f' : '#e74c3c');
const bar = `<span style="display:inline-block;vertical-align:middle;width:120px;height:8px;background:#1e3a5f;border-radius:4px;margin:0 8px;overflow:hidden"><span style="display:block;height:100%;width:${pct}%;background:${color}"></span></span>`;
return `<div style="padding:8px 4px;border-bottom:1px solid #16233a">
<b>${t.badge ? t.badge + ' ' : '#' + t.rank + ' '}</b>${esc(t.label)} ${bar}<span style="color:${color}">${t.alive}/${t.total} (${pct}%)</span>
<span style="float:right;color:#5ad1ff">⚑ ${t.solves} flag · 🏆 ${t.points} pts</span>
</div>`;
}).join('');
box.innerHTML = rows || '<div style="color:#718096">Belum ada tim.</div>';
} catch (e) {
box.innerHTML = '<div style="color:#e74c3c">Gagal memuat SLA.</div>';
}
}
// ---------- Challenges ----------
async function refresh() {
+48 -1
View File
@@ -118,6 +118,7 @@
<button data-view="submit" onclick="showView('submit')">🚩 Submit Flag</button>
<button data-view="activity" onclick="showView('activity')">📡 Aktivitas</button>
<button data-view="leaderboard" onclick="showView('leaderboard')">🏆 Leaderboard</button>
<button data-view="sla" onclick="showView('sla'); loadSla()">📊 SLA & Skor</button>
<button data-view="guide" onclick="showView('guide')">📖 Panduan SSH</button>
</div>
@@ -194,6 +195,16 @@
</div>
</div>
<div class="view" id="view-sla">
<div class="card">
<h2>📊 SLA & Skor Tim</h2>
<p style="font-size:13px;color:var(--dim);margin-bottom:12px">
💚 <b>SLA</b> = service timmu hidup (dicek tiap ~30 detik) · 🏆 <b>poin</b> = dapat dari mencuri flag lawan (+100/flag) & bonus SLA saat seluruh service UP (+50).
</p>
<div id="slaList"><div class="lb-empty">Memuat…</div></div>
</div>
</div>
<div class="view" id="view-guide">
<div class="card guide">
<h2>📖 Panduan SSH & Attack</h2>
@@ -254,6 +265,7 @@ function showView(v) {
if (v === 'targets') loadTargets();
if (v === 'activity') loadActivity();
if (v === 'leaderboard') loadLeaderboard();
if (v === 'sla') loadSla();
}
// ---------- auth ----------
@@ -425,6 +437,41 @@ async function loadActivity() {
}
}
// ---------- SLA & scoreboard (public endpoint) ----------
async function loadSla() {
const box = document.getElementById('slaList');
try {
const d = await api('/api/public/scoreboard');
const teams = d.teams || [];
if (!teams.length) {
box.innerHTML = '<div class="lb-empty">Belum ada data SLA.</div>';
return;
}
box.innerHTML = teams.map(t => {
const isMe = t.team === TEAM_ID;
const pct = Math.round(t.sla_pct || 0);
const color = pct === 100 ? '#2ecc71' : (pct >= 60 ? '#f1c40f' : '#e74c3c');
const bar = `<div style="height:8px;background:#1e2a3a;border-radius:4px;margin:6px 0;overflow:hidden"><div style="height:100%;width:${pct}%;background:${color};transition:width .5s"></div></div>`;
return `<div class="lb-row ${isMe ? 'me' : ''}">
<span class="lb-rank" style="width:34px">${esc(t.badge || t.rank)}</span>
<span class="lb-name">${esc(t.label)} ${isMe ? '<small>(kamu)</small>' : ''}</span>
<span class="lb-solves" style="width:170px;text-align:right">
<b style="color:${color}">${t.alive}/${t.total} (${pct}%)</b>
<br><small style="color:var(--dim)">⚑ ${t.solves} flag · 🏆 ${t.points} pts</small>
</span>
</div>${bar}`;
}).join('');
} catch (e) {
box.innerHTML = '<div class="lb-empty">Gagal memuat SLA.</div>';
}
}
// auto-refresh SLA while its tab is open (every 30s)
setInterval(async () => {
const s = document.getElementById('view-sla');
if (s && s.classList.contains('active')) loadSla();
}, 30000);
// ---------- leaderboard (public endpoint) ----------
async function loadLeaderboard() {
const box = document.getElementById('lbList');
@@ -442,7 +489,7 @@ async function loadLeaderboard() {
return `<div class="lb-row ${isMe ? 'me' : ''}">
<span class="lb-rank">${rank}</span>
<span class="lb-name">${esc(t.name)} ${isMe ? '<small>(kamu)</small>' : ''}</span>
<span class="lb-solves">⚑ ${t.solves} solve${t.solves > 1 ? 's' : ''}</span>
<span class="lb-solves">⚑ ${t.solves} solve${t.solves > 1 ? 's' : ''} · 🏆 ${t.points ?? 0} pts</span>
</div>`;
}).join('');
const found = teams.some(t => t.team === TEAM_ID);