feat: team activity feed (attacks in/out) + activity tab + auto-refresh + leaderboard tab on team portal
This commit is contained in:
+124
-4
@@ -60,6 +60,29 @@
|
||||
.guide ol, .guide ul { padding-left:20px; }
|
||||
.login-note { font-size:12px; color:var(--dim); margin-top:14px; text-align:center; }
|
||||
.login-note a { color:var(--accent); }
|
||||
/* activity feed */
|
||||
.feed { display:flex; flex-direction:column; gap:10px; max-height:420px; overflow-y:auto; }
|
||||
.feed-item { background:var(--panel2); border:1px solid var(--line); border-left:3px solid var(--line); border-radius:10px; padding:10px 14px; font-size:13px; line-height:1.5; }
|
||||
.feed-item.in { border-left-color:var(--red); }
|
||||
.feed-item.out { border-left-color:var(--green); }
|
||||
.feed-item.ok { border-left-color:var(--green); }
|
||||
.feed-item.fail { border-left-color:var(--red); }
|
||||
.feed-time { color:var(--dim); font-size:11px; margin-left:8px; }
|
||||
.feed-tag { font-size:11px; border-radius:5px; padding:1px 7px; margin-right:6px; font-weight:700; }
|
||||
.tag-in { background:#3a1414; color:var(--red); }
|
||||
.tag-out { background:#06352a; color:var(--green); }
|
||||
code { background:var(--panel2); padding:2px 6px; border-radius:5px; font-size:12px; }
|
||||
.lb-row { display:flex; align-items:center; gap:12px; padding:10px 12px; border-bottom:1px solid var(--line); font-size:14px; }
|
||||
.lb-row.me { background:rgba(56,189,248,.08); border-left:3px solid var(--accent); }
|
||||
.lb-rank { width:28px; font-weight:800; color:var(--dim); }
|
||||
.lb-name { flex:1; font-weight:600; }
|
||||
.lb-name small { color:var(--dim); font-weight:400; margin-left:6px; }
|
||||
.lb-solves { background:var(--panel2); border:1px solid var(--line); border-radius:20px; padding:3px 12px; font-size:12px; color:var(--green); }
|
||||
.lb-empty { color:var(--dim); font-size:13px; padding:20px; text-align:center; }
|
||||
.svc-health { display:flex; flex-wrap:wrap; gap:8px; margin-top:12px; }
|
||||
.svc-chip { border:1px solid var(--line); border-radius:8px; padding:4px 10px; font-size:12px; background:var(--panel2); }
|
||||
.svc-chip.up { border-color:var(--green); color:var(--green); }
|
||||
.svc-chip.down { border-color:var(--red); color:var(--red); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -90,10 +113,12 @@
|
||||
<!-- PORTAL -->
|
||||
<div id="portalScreen" style="display:none">
|
||||
<div class="tabs">
|
||||
<button class="active" data-view="term" onclick="showView('term')">🖥️ Terminal SSH</button>
|
||||
<button data-view="targets" onclick="showView('targets')">🎯 Target Musuh</button>
|
||||
<button data-view="submit" onclick="showView('submit')">🚩 Submit Flag</button>
|
||||
<button data-view="guide" onclick="showView('guide')">📖 Panduan SSH</button>
|
||||
<button data-view="term" onclick="showView('term')">🖥️ Terminal SSH</button>
|
||||
<button data-view="targets" onclick="showView('targets')">🎯 Target Musuh</button>
|
||||
<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="guide" onclick="showView('guide')">📖 Panduan SSH</button>
|
||||
</div>
|
||||
|
||||
<div class="view active" id="view-term">
|
||||
@@ -149,6 +174,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="view" id="view-activity">
|
||||
<div class="card">
|
||||
<h2>📡 Aktivitas & Serangan</h2>
|
||||
<p style="font-size:13px;color:var(--dim);margin-bottom:12px">
|
||||
🔴 <b>masuk</b> = ada yang menyerang service timmu · 🟢 <b>keluar</b> = kamu menyerang tim lain.
|
||||
Update otomatis tiap 15 detik.
|
||||
</p>
|
||||
<div id="activityFeed" class="feed"><div class="lb-empty">Memuat aktivitas…</div></div>
|
||||
<div class="svc-health" id="svcHealth"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="view" id="view-leaderboard">
|
||||
<div class="card">
|
||||
<h2>🏆 Leaderboard</h2>
|
||||
<p style="font-size:13px;color:var(--dim);margin-bottom:12px">Papan skor flag yang berhasil dicuri. Kamu = baris biru.</p>
|
||||
<div id="lbList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="view" id="view-guide">
|
||||
<div class="card guide">
|
||||
<h2>📖 Panduan SSH & Attack</h2>
|
||||
@@ -207,6 +252,8 @@ function showView(v) {
|
||||
document.querySelectorAll('.view').forEach(x => x.classList.toggle('active', x.id === 'view-' + v));
|
||||
if (v === 'term' && term) term.focus();
|
||||
if (v === 'targets') loadTargets();
|
||||
if (v === 'activity') loadActivity();
|
||||
if (v === 'leaderboard') loadLeaderboard();
|
||||
}
|
||||
|
||||
// ---------- auth ----------
|
||||
@@ -340,6 +387,79 @@ async function loadTargetDropdown() {
|
||||
}
|
||||
} catch (e) { /* non-fatal */ }
|
||||
}
|
||||
|
||||
// ---------- activity feed (attacks involving this team) ----------
|
||||
async function loadActivity() {
|
||||
const box = document.getElementById('activityFeed');
|
||||
const healthBox = document.getElementById('svcHealth');
|
||||
try {
|
||||
const d = await api(`/api/team/${TEAM_ID}/activity`);
|
||||
const events = d.events || [];
|
||||
if (!events.length) {
|
||||
box.innerHTML = '<div class="lb-empty">Belum ada aktivitas serangan. Aman… untuk sekarang 😌</div>';
|
||||
} else {
|
||||
box.innerHTML = events.map(e => {
|
||||
const inbound = e.target === TEAM_ID;
|
||||
const cls = inbound ? 'in' : 'out';
|
||||
const tag = inbound
|
||||
? `<span class="feed-tag tag-in">⬇ MASUK</span>${esc(e.attacker_label)} menyerang`
|
||||
: `<span class="feed-tag tag-out">⬆ KELUAR</span>kamu menyerang ${esc(e.target_label)}`;
|
||||
const verdict = e.success
|
||||
? '<span style="color:var(--green)">✅ flag benar</span>'
|
||||
: '<span style="color:var(--red)">❌ flag salah</span>';
|
||||
return `<div class="feed-item ${cls}">${tag} <code>${esc(e.challenge || '')}</code> ${verdict}` +
|
||||
`<span class="feed-time">${esc(e.ts_human || '')}</span></div>`;
|
||||
}).join('');
|
||||
}
|
||||
// own service health chips (all values escaped above via esc())
|
||||
try {
|
||||
const st = await api(`/api/team/${TEAM_ID}/status`);
|
||||
if (st.results) {
|
||||
healthBox.innerHTML = st.results.map(r =>
|
||||
`<span class="svc-chip ${r.alive ? 'up' : 'down'}">${esc(r.name)} ${r.alive ? '● UP' : '○ DOWN'} :${r.port}</span>`
|
||||
).join('');
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
box.innerHTML = '<div class="lb-empty">Gagal memuat aktivitas.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- leaderboard (public endpoint) ----------
|
||||
async function loadLeaderboard() {
|
||||
const box = document.getElementById('lbList');
|
||||
try {
|
||||
const d = await api('/api/leaderboard');
|
||||
const teams = d.teams || [];
|
||||
if (!teams.length) {
|
||||
box.innerHTML = '<div class="lb-empty">Belum ada flag yang berhasil dicuri. Jadilah yang pertama! 🚀</div>';
|
||||
return;
|
||||
}
|
||||
const medal = ['🥇','🥈','🥉'];
|
||||
box.innerHTML = teams.map((t, i) => {
|
||||
const isMe = t.team === TEAM_ID;
|
||||
const rank = i < 3 ? medal[i] : `<span class="lb-rank" style="width:28px">${i+1}</span>`;
|
||||
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>
|
||||
</div>`;
|
||||
}).join('');
|
||||
const found = teams.some(t => t.team === TEAM_ID);
|
||||
if (!found) {
|
||||
box.innerHTML += `<div class="lb-row me"><span class="lb-rank">—</span><span class="lb-name">Tim kamu <small>(kamu)</small></span><span class="lb-solves">⚑ 0 solve</span></div>`;
|
||||
}
|
||||
} catch (e) {
|
||||
box.innerHTML = '<div class="lb-empty">Gagal memuat leaderboard.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// auto-refresh activity while its tab is open (every 15s)
|
||||
setInterval(async () => {
|
||||
const a = document.getElementById('view-activity');
|
||||
if (a && a.classList.contains('active')) loadActivity();
|
||||
}, 15000);
|
||||
|
||||
loadTargetDropdown();
|
||||
|
||||
checkSession();
|
||||
|
||||
Reference in New Issue
Block a user