feat: team activity feed (attacks in/out) + activity tab + auto-refresh + leaderboard tab on team portal

This commit is contained in:
root
2026-09-23 22:56:07 +08:00
parent 877f14ecf3
commit 3ef905b3bb
2 changed files with 160 additions and 4 deletions
+36
View File
@@ -278,6 +278,42 @@ async def api_team_status(idx: int, req: Request):
"ssh": st["ports"][name]["ssh"], "alive": ok})
return {"results": results}
@app.get("/api/team/{idx}/activity")
async def api_team_activity(idx: int, req: Request):
"""Team-scoped activity feed: attack events where this team is attacker or
target (i.e. "ada serangan ke service kita" / "kita menyerang"), plus a
snapshot of own service health. Requires team login + own host."""
td = orch.TEAMS_DIR / f"team{idx}"
if not (td / "state.json").exists():
raise HTTPException(404, "Team not found")
st_self = json.loads((td / "state.json").read_text())
if not _check_team_host(req, st_self):
raise HTTPException(403, "Akses team lain tidak diizinkan")
if not _team_authorized(req, idx):
raise HTTPException(401, "Login portal team dulu")
# live label lookup (rename team = activity updates everywhere)
def label(i):
try:
s = json.loads((orch.TEAMS_DIR / f"team{i}" / "state.json").read_text())
return s.get("label") or f"Team {i}"
except Exception:
return f"Team {i}"
ap = orch.TEAMS_DIR / "attacks.json"
try:
events = json.loads(ap.read_text()).get("events", []) if ap.exists() else []
except Exception:
events = []
mine = []
for e in reversed(events): # newest first
if e.get("attacker") == idx or e.get("target") == idx:
e = dict(e)
e["attacker_label"] = label(e.get("attacker"))
e["target_label"] = label(e.get("target"))
mine.append(e)
return {"events": mine[:100], "team_label": st_self.get("label") or f"Team {idx}"}
@app.post("/api/login")
async def api_login(req: Request):
data = await req.json()
+124 -4
View File
@@ -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 &amp; 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();