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() {