From 3ef905b3bbc5543325c62e4907faca0b25a7c941 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 23 Sep 2026 22:56:07 +0800 Subject: [PATCH] feat: team activity feed (attacks in/out) + activity tab + auto-refresh + leaderboard tab on team portal --- panel/main.py | 36 ++++++++++++ panel/static/team.html | 128 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 160 insertions(+), 4 deletions(-) diff --git a/panel/main.py b/panel/main.py index 9f30709..837a299 100644 --- a/panel/main.py +++ b/panel/main.py @@ -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() diff --git a/panel/static/team.html b/panel/static/team.html index 22f3c6b..e123a6a 100644 --- a/panel/static/team.html +++ b/panel/static/team.html @@ -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); } @@ -90,10 +113,12 @@ +
+
+

πŸ“‘ Aktivitas & Serangan

+

+ πŸ”΄ masuk = ada yang menyerang service timmu Β· 🟒 keluar = kamu menyerang tim lain. + Update otomatis tiap 15 detik. +

+
Memuat aktivitas…
+
+
+
+ +
+
+

πŸ† Leaderboard

+

Papan skor flag yang berhasil dicuri. Kamu = baris biru.

+
+
+
+

πŸ“– Panduan SSH & Attack

@@ -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 = '
Belum ada aktivitas serangan. Aman… untuk sekarang 😌
'; + } else { + box.innerHTML = events.map(e => { + const inbound = e.target === TEAM_ID; + const cls = inbound ? 'in' : 'out'; + const tag = inbound + ? `⬇ MASUK${esc(e.attacker_label)} menyerang` + : `⬆ KELUARkamu menyerang ${esc(e.target_label)}`; + const verdict = e.success + ? 'βœ… flag benar' + : '❌ flag salah'; + return `
${tag} ${esc(e.challenge || '')} ${verdict}` + + `${esc(e.ts_human || '')}
`; + }).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 => + `${esc(r.name)} ${r.alive ? '● UP' : 'β—‹ DOWN'} :${r.port}` + ).join(''); + } + } catch (e) {} + } catch (e) { + box.innerHTML = '
Gagal memuat aktivitas.
'; + } +} + +// ---------- 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 = '
Belum ada flag yang berhasil dicuri. Jadilah yang pertama! πŸš€
'; + return; + } + const medal = ['πŸ₯‡','πŸ₯ˆ','πŸ₯‰']; + box.innerHTML = teams.map((t, i) => { + const isMe = t.team === TEAM_ID; + const rank = i < 3 ? medal[i] : `${i+1}`; + return `
+ ${rank} + ${esc(t.name)} ${isMe ? '(kamu)' : ''} + βš‘ ${t.solves} solve${t.solves > 1 ? 's' : ''} +
`; + }).join(''); + const found = teams.some(t => t.team === TEAM_ID); + if (!found) { + box.innerHTML += `
β€”Tim kamu (kamu)βš‘ 0 solve
`; + } + } catch (e) { + box.innerHTML = '
Gagal memuat leaderboard.
'; + } +} + +// 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();