2026-09-23 16:18:11 +08:00
<!DOCTYPE html>
< html lang = "id" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
2026-09-23 16:37:58 +08:00
< meta name = "team-id" content = "0" >
2026-09-25 14:04:33 +08:00
< title > Portal Tim — Attack Defense Platform</ title >
2026-09-23 16:37:58 +08:00
< link rel = "stylesheet" href = "https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css" >
< script src = "https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js" ></ script >
2026-09-23 16:18:11 +08:00
< style >
2026-09-23 16:37:58 +08:00
: root { --bg : #0b0e14 ; --panel : #151a23 ; --panel2 : #1d2430 ; --line : #2a3444 ; --txt : #e6edf3 ; --dim : #8b98a9 ; --accent : #38bdf8 ; --green : #34d399 ; --red : #f87171 ; --purple : #a78bfa ; }
2026-09-23 16:18:11 +08:00
* { margin : 0 ; padding : 0 ; box-sizing : border-box ; }
body { background : var ( -- bg ); color : var ( -- txt ); font-family : 'Segoe UI' , system-ui , sans-serif ; min-height : 100 vh ; }
2026-09-23 16:37:58 +08:00
. wrap { max-width : 1000 px ; margin : 0 auto ; padding : 24 px 16 px 60 px ; }
2026-09-23 16:18:11 +08:00
header { display : flex ; align-items : center ; gap : 12 px ; margin-bottom : 24 px ; flex-wrap : wrap ; }
. logo { width : 42 px ; height : 42 px ; border-radius : 10 px ; background : linear-gradient ( 135 deg , #38bdf8 , #6366f1 ); display : flex ; align-items : center ; justify-content : center ; font-weight : 800 ; font-size : 20 px ; color : #fff ; }
h1 { font-size : 22 px ; }
. sub { color : var ( -- dim ); font-size : 13 px ; }
. badge { background : #06352a ; border : 1 px solid var ( -- green ); color : var ( -- green ); border-radius : 20 px ; padding : 3 px 12 px ; font-size : 12 px ; font-weight : 600 ; }
2026-09-23 16:37:58 +08:00
. badge . stopped { background : #3a1414 ; border-color : var ( -- red ); color : var ( -- red ); }
2026-09-23 16:18:11 +08:00
. card { background : var ( -- panel ); border : 1 px solid var ( -- line ); border-radius : 14 px ; padding : 20 px ; margin-bottom : 16 px ; }
. card h2 { font-size : 15 px ; margin-bottom : 12 px ; color : var ( -- accent ); }
. kv { display : grid ; grid-template-columns : 150 px 1 fr ; gap : 6 px 12 px ; font-size : 13 px ; margin-bottom : 4 px ; }
. kv b { color : var ( -- dim ); font-weight : 500 ; }
. kv span { font-family : ui-monospace , monospace ; }
table { width : 100 % ; border-collapse : collapse ; font-size : 13 px ; }
th , td { text-align : left ; padding : 8 px 6 px ; border-bottom : 1 px solid var ( -- line ); }
th { color : var ( -- dim ); font-weight : 500 ; }
. dot { display : inline-block ; width : 8 px ; height : 8 px ; border-radius : 50 % ; margin-right : 6 px ; }
. dot . up { background : var ( -- green ); } . dot . down { background : var ( -- red ); }
label { display : block ; font-size : 12 px ; color : var ( -- dim ); margin : 12 px 0 5 px ; }
2026-09-23 16:37:58 +08:00
select , input [ type = text ], input [ type = password ] { width : 100 % ; padding : 10 px 12 px ; border-radius : 8 px ; border : 1 px solid var ( -- line ); background : var ( -- panel2 ); color : var ( -- txt ); font-size : 14 px ; }
button { padding : 12 px 16 px ; border : none ; border-radius : 8 px ; background : linear-gradient ( 135 deg , #38bdf8 , #6366f1 ); color : #fff ; font-weight : 600 ; font-size : 14 px ; cursor : pointer ; }
2026-09-23 16:18:11 +08:00
button : hover { filter : brightness ( 1.1 ); }
2026-09-23 16:37:58 +08:00
button . ghost { background : var ( -- panel2 ); border : 1 px solid var ( -- line ); color : var ( -- txt ); }
2026-09-23 16:18:11 +08:00
# result { margin-top : 14 px ; padding : 12 px ; border-radius : 8 px ; display : none ; font-size : 14 px ; }
# result . ok { display : block ; background : #06352a ; border : 1 px solid var ( -- green ); color : var ( -- green ); }
# result . err { display : block ; background : #3a1414 ; border : 1 px solid var ( -- red ); color : var ( -- red ); }
. sshbox { background : var ( -- panel2 ); border : 1 px solid var ( -- line ); border-radius : 8 px ; padding : 10 px 12 px ; font-family : ui-monospace , monospace ; font-size : 12 px ; overflow-x : auto ; }
2026-09-23 16:37:58 +08:00
/* login */
# loginScreen { max-width : 420 px ; margin : 80 px auto ; }
# loginScreen . card { padding : 32 px ; }
# loginScreen h2 { text-align : center ; margin-bottom : 6 px ; }
# loginScreen . hint { text-align : center ; color : var ( -- dim ); font-size : 12 px ; margin-bottom : 18 px ; }
. login-err { color : var ( -- red ); font-size : 13 px ; margin-top : 10 px ; display : none ; }
/* tabs */
. tabs { display : flex ; gap : 8 px ; margin-bottom : 16 px ; flex-wrap : wrap ; }
. tabs button { background : var ( -- panel2 ); border : 1 px solid var ( -- line ); color : var ( -- dim ); font-weight : 500 ; }
. tabs button . active { background : linear-gradient ( 135 deg , #38bdf8 , #6366f1 ); color : #fff ; border-color : transparent ; }
. view { display : none ; }
. view . active { display : block ; }
/* terminal */
# termWrap { background : #0d1117 ; border : 1 px solid var ( -- line ); border-radius : 10 px ; padding : 8 px ; height : 480 px ; }
# term { height : 100 % ; }
. term-toolbar { display : flex ; gap : 8 px ; align-items : center ; margin-bottom : 10 px ; flex-wrap : wrap ; }
# termStatus { font-size : 12 px ; color : var ( -- dim ); }
code { background : var ( -- panel2 ); padding : 2 px 6 px ; border-radius : 5 px ; font-size : 12 px ; }
. guide h3 { color : var ( -- purple ); margin : 18 px 0 8 px ; font-size : 14 px ; }
. guide p , . guide li { font-size : 13 px ; color : #c9d4e3 ; line-height : 1.7 ; }
. guide ol , . guide ul { padding-left : 20 px ; }
. login-note { font-size : 12 px ; color : var ( -- dim ); margin-top : 14 px ; text-align : center ; }
. login-note a { color : var ( -- accent ); }
2026-09-23 22:56:07 +08:00
/* activity feed */
. feed { display : flex ; flex-direction : column ; gap : 10 px ; max-height : 420 px ; overflow-y : auto ; }
. feed-item { background : var ( -- panel2 ); border : 1 px solid var ( -- line ); border-left : 3 px solid var ( -- line ); border-radius : 10 px ; padding : 10 px 14 px ; font-size : 13 px ; 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 : 11 px ; margin-left : 8 px ; }
. feed-tag { font-size : 11 px ; border-radius : 5 px ; padding : 1 px 7 px ; margin-right : 6 px ; font-weight : 700 ; }
. tag-in { background : #3a1414 ; color : var ( -- red ); }
. tag-out { background : #06352a ; color : var ( -- green ); }
code { background : var ( -- panel2 ); padding : 2 px 6 px ; border-radius : 5 px ; font-size : 12 px ; }
. lb-row { display : flex ; align-items : center ; gap : 12 px ; padding : 10 px 12 px ; border-bottom : 1 px solid var ( -- line ); font-size : 14 px ; }
. lb-row . me { background : rgba ( 56 , 189 , 248 , .08 ); border-left : 3 px solid var ( -- accent ); }
. lb-rank { width : 28 px ; font-weight : 800 ; color : var ( -- dim ); }
. lb-name { flex : 1 ; font-weight : 600 ; }
. lb-name small { color : var ( -- dim ); font-weight : 400 ; margin-left : 6 px ; }
. lb-solves { background : var ( -- panel2 ); border : 1 px solid var ( -- line ); border-radius : 20 px ; padding : 3 px 12 px ; font-size : 12 px ; color : var ( -- green ); }
. lb-empty { color : var ( -- dim ); font-size : 13 px ; padding : 20 px ; text-align : center ; }
. svc-health { display : flex ; flex-wrap : wrap ; gap : 8 px ; margin-top : 12 px ; }
. svc-chip { border : 1 px solid var ( -- line ); border-radius : 8 px ; padding : 4 px 10 px ; font-size : 12 px ; background : var ( -- panel2 ); }
. svc-chip . up { border-color : var ( -- green ); color : var ( -- green ); }
. svc-chip . down { border-color : var ( -- red ); color : var ( -- red ); }
2026-09-23 16:18:11 +08:00
</ style >
</ head >
< body >
< div class = "wrap" >
< header >
< div class = "logo" > G</ div >
< div style = "flex:1" >
< h1 id = "teamTitle" > Portal Tim</ h1 >
< div class = "sub" id = "teamDomain" ></ div >
</ div >
< span class = "badge" id = "teamStatus" > …</ span >
2026-09-23 16:37:58 +08:00
< button class = "ghost" id = "logoutBtn" style = "display:none" onclick = "logout()" > Keluar</ button >
2026-09-23 16:18:11 +08:00
</ header >
2026-09-23 16:37:58 +08:00
<!-- LOGIN -->
< div id = "loginScreen" >
< div class = "card" >
< h2 > 🔐 Login Portal Tim</ h2 >
< div class = "hint" id = "loginHint" > Masukkan < b > password SSH tim</ b > kamu (dari panitia)</ div >
< label > Password Tim</ label >
< input type = "password" id = "loginPass" placeholder = "password SSH tim" autocomplete = "off" >
< button onclick = "doLogin()" style = "width:100%;margin-top:16px" > Masuk Portal</ button >
< div class = "login-err" id = "loginErr" ></ div >
2026-09-23 17:02:52 +08:00
< div class = "login-note" > Belum punya password? Hubungi panitia.< br > Tutorial SSH: < a href = "/team/0/guide" data-guide-link > baca panduan</ a ></ div >
2026-09-23 16:18:11 +08:00
</ div >
</ div >
2026-09-23 16:37:58 +08:00
<!-- PORTAL -->
< div id = "portalScreen" style = "display:none" >
< div class = "tabs" >
2026-09-23 22:56:07 +08:00
< 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 >
2026-09-24 00:49:05 +08:00
< button data-view = "sla" onclick = "showView('sla'); loadSla()" > 📊 SLA & Skor</ button >
2026-09-23 22:56:07 +08:00
< button data-view = "guide" onclick = "showView('guide')" > 📖 Panduan SSH</ button >
2026-09-23 16:37:58 +08:00
</ div >
2026-09-23 16:18:11 +08:00
2026-09-23 16:37:58 +08:00
< div class = "view active" id = "view-term" >
< div class = "card" >
< h2 > 🖥️ SSH Terminal — pilih challenge</ h2 >
< div class = "term-toolbar" >
< select id = "termChall" style = "width:200px" onchange = "connectTerm()" >
< option value = "blogpost" > blogpost (web)</ option >
< option value = "carbeat" > carbeat (pwn)</ option >
< option value = "cdn" > cdn (web)</ option >
< option value = "phew" > phew (crypto)</ option >
< option value = "sheesh" > sheesh (crypto)</ option >
< option value = "warmup" > warmup</ option >
</ select >
< button class = "ghost" onclick = "connectTerm()" > 🔄 Sambung</ button >
< span id = "termStatus" > Belum tersambung</ span >
</ div >
< div id = "termWrap" >< div id = "term" ></ div ></ div >
< div style = "margin-top:10px;font-size:12px;color:var(--dim)" >
SSH otomatis login sebagai < code > ctfuser</ code > ke container challenge timmu. Koneksi diputus setelah idle.
</ div >
</ div >
</ div >
< div class = "view" id = "view-targets" >
< div class = "card" >
< h2 > 🎯 Target Musuh</ h2 >
< p style = "font-size:13px;color:var(--dim);margin-bottom:12px" > Service challenge tim lain — inilah yang harus kamu attack & curi flagnya.</ p >
< div id = "targetList" class = "sshbox" > Memuat…</ div >
</ div >
</ div >
< div class = "view" id = "view-submit" >
< div class = "card" >
< h2 > 🚩 Submit Flag</ h2 >
< label > Challenge</ label >
< select id = "challenge" >
< option value = "blogpost" > blogpost (web)</ option >
< option value = "carbeat" > carbeat (pwn)</ option >
< option value = "cdn" > cdn (web)</ option >
< option value = "phew" > phew (crypto)</ option >
< option value = "sheesh" > sheesh (crypto)</ option >
< option value = "warmup" > warmup</ option >
</ select >
2026-09-23 18:05:44 +08:00
< label > Target (tim musuh yang diserang)</ label >
< select id = "target" >
< option value = "" > Memuat target…</ option >
</ select >
< label > Flag (yang kamu curi dari target)</ label >
2026-09-23 16:37:58 +08:00
< input type = "text" id = "flag" placeholder = "GEMASTIK18{...}" autocomplete = "off" >
< button onclick = "submitFlag()" style = "width:100%;margin-top:16px" > Submit Flag</ button >
< div id = "result" ></ div >
</ div >
</ div >
2026-09-23 22:56:07 +08:00
< 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 >
2026-09-24 00:49:05 +08:00
< 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 >
2026-09-23 16:37:58 +08:00
< div class = "view" id = "view-guide" >
< div class = "card guide" >
< h2 > 📖 Panduan SSH & Attack</ h2 >
< h3 > 1. Apa itu SSH di sini?</ h3 >
< p > Setiap challenge punya service yang harus kamu < b > pertahankan</ b > (defense) dan service tim lain yang harus kamu < b > serang</ b > (attack). SSH container adalah akses administratif ke challenge tim kamu — untuk riset, patch, atau recovery.</ p >
< h3 > 2. Login via Web Terminal</ h3 >
< ol >
< li > Buka tab < b > 🖥️ Terminal SSH</ b > .</ li >
< li > Pilih challenge (misal < code > blogpost</ code > ).</ li >
< li > Klik < b > Sambung</ b > — otomatis login sebagai < code > ctfuser</ code > .</ li >
</ ol >
< h3 > 3. Login via SSH biasa (opsional)</ h3 >
< div class = "sshbox" > ssh ctfuser@43.134.105.109 -p < PORT_SSH>
# password = password tim kamu</ div >
< h3 > 4. Command yang berguna</ h3 >
< ul >
< li >< code > cat /flag.txt</ code > — flag < b > tim kamu sendiri</ b > (jangan bocorin!)</ li >
< li >< code > ls /app /srv /opt</ code > — cari source code challenge</ li >
< li >< code > ps aux</ code > — lihat proses service</ li >
< li >< code > cat /flag.txt | nc < ip_tim_lawan> < port_chall> </ code > — kirim flag kamu 🤫</ li >
</ ul >
< h3 > 5. Alur Attack</ h3 >
< ol >
< li > Cari vuln di challenge (SSTI, path traversal, crypto oracle, dll).</ li >
< li > Exploit service < b > tim lawan</ b > — lihat tab < b > 🎯 Target Musuh</ b > .</ li >
< li > Baca flag dari container/service lawan.</ li >
< li > Submit flag di tab < b > 🚩 Submit Flag</ b > → dapat poin.</ li >
</ ol >
< h3 > 6. Aturan</ h3 >
< ul >
< li > Jangan ubah flag tim sendiri (mount read-only).</ li >
< li > Jangan matikan service tim sendiri — SLA dicek panitia.</ li >
< li > Flag yang valid: < code > GEMASTIK18{...}</ code > .</ li >
</ ul >
</ div >
</ div >
2026-09-23 16:18:11 +08:00
</ div >
</ div >
< script >
const TEAM_ID = parseInt ( document . querySelector ( 'meta[name="team-id"]' ). content || '0' , 10 );
2026-09-23 16:37:58 +08:00
let term = null , ws = null ;
2026-09-23 16:18:11 +08:00
2026-09-23 17:02:52 +08:00
// fix all guide links immediately (no auth needed) — /team/0 would 403
document . querySelectorAll ( '[data-guide-link]' ). forEach ( a => a . href = `/team/ ${ TEAM_ID } /guide` );
2026-09-23 16:18:11 +08:00
async function api ( url , opts ) { const r = await fetch ( url , opts ); return r . json (); }
2026-09-23 16:37:58 +08:00
function esc ( s ) { return String ( s ?? '' ). replace ( /[&<>"']/g , c => ({ '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' }[ c ])); }
function showView ( v ) {
document . querySelectorAll ( '.tabs button' ). 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 === 'term' && term ) term . focus ();
if ( v === 'targets' ) loadTargets ();
2026-09-23 22:56:07 +08:00
if ( v === 'activity' ) loadActivity ();
if ( v === 'leaderboard' ) loadLeaderboard ();
2026-09-24 00:49:05 +08:00
if ( v === 'sla' ) loadSla ();
2026-09-23 16:37:58 +08:00
}
2026-09-23 16:18:11 +08:00
2026-09-23 16:37:58 +08:00
// ---------- auth ----------
async function checkSession () {
const s = await api ( `/api/team/ ${ TEAM_ID } /session` );
if ( s . authed ) {
document . getElementById ( 'loginScreen' ). style . display = 'none' ;
document . getElementById ( 'portalScreen' ). style . display = 'block' ;
document . getElementById ( 'logoutBtn' ). style . display = '' ;
loadInfo ();
2026-09-23 16:18:11 +08:00
}
}
2026-09-23 16:37:58 +08:00
async function doLogin () {
const pass = document . getElementById ( 'loginPass' ). value ;
const err = document . getElementById ( 'loginErr' );
err . style . display = 'none' ;
const d = await api ( `/api/team/ ${ TEAM_ID } /login` , {
method : 'POST' , headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ({ pass }),
});
if ( d . ok ) {
document . getElementById ( 'loginScreen' ). style . display = 'none' ;
document . getElementById ( 'portalScreen' ). style . display = 'block' ;
document . getElementById ( 'logoutBtn' ). style . display = '' ;
loadInfo ();
2026-09-23 18:05:44 +08:00
loadTargetDropdown (); // target dropdown needs auth — refresh after login
loadTargets ();
2026-09-23 16:37:58 +08:00
} else {
err . textContent = 'Password salah. Coba lagi.' ;
err . style . display = 'block' ;
}
}
async function logout () {
await api ( '/api/team/logout' , { method : 'POST' });
location . reload ();
}
// ---------- info & targets ----------
async function loadInfo () {
try {
const t = ( await api ( `/api/team/ ${ TEAM_ID } /info` )). team ;
document . getElementById ( 'teamTitle' ). textContent = t . label || ( 'Team ' + TEAM_ID );
document . getElementById ( 'teamDomain' ). textContent = t . domain || '' ;
const st = document . getElementById ( 'teamStatus' );
if ( t . status === 'running' ) {
st . textContent = '● RUNNING' ; st . className = 'badge' ;
} else {
st . textContent = '○ STOPPED' ; st . className = 'badge stopped' ;
}
document . getElementById ( 'loginHint' ). innerHTML = 'Masukkan <b>password SSH tim</b> kamu (dari panitia)' ;
document . getElementById ( 'guideLink' ). href = `/team/ ${ TEAM_ID } /guide` ;
} catch ( e ) {}
}
async function loadTargets () {
const box = document . getElementById ( 'targetList' );
const d = await api ( `/api/team/ ${ TEAM_ID } /targets` );
const targets = d . targets || [];
if ( ! targets . length ) { box . textContent = 'Belum ada tim musuh.' ; return ; }
let html = '=== TARGET MUSUH ===\n\n' ;
for ( const t of targets ) {
2026-09-23 17:14:42 +08:00
html += ` ${ esc ( t . domain ) } : ${ t . port } \n` ;
2026-09-23 16:37:58 +08:00
}
box . textContent = html ;
}
// ---------- SSH terminal ----------
function connectTerm () {
const chall = document . getElementById ( 'termChall' ). value ;
const status = document . getElementById ( 'termStatus' );
const proto = location . protocol === 'https:' ? 'wss:' : 'ws:' ;
const url = ` ${ proto } // ${ location . host } /api/team/ ${ TEAM_ID } /ssh/ws?chall= ${ chall } ` ;
if ( ws ) { try { ws . close (); } catch ( e ){} }
if ( term ) term . dispose ();
term = new Terminal ({ cursorBlink : true , fontSize : 13 , theme : { background : '#0d1117' }});
term . open ( document . getElementById ( 'term' ));
ws = new WebSocket ( url );
status . textContent = 'Menghubungkan…' ;
ws . onopen = () => { status . textContent = `● tersambung ke ${ chall } (ctfuser)` ; term . focus (); };
ws . onmessage = ev => term . write ( ev . data );
ws . onclose = ev => { status . textContent = '✖ terputus (' + ( ev . reason || 'closed' ) + ')' ; };
ws . onerror = () => { status . textContent = '✖ error koneksi' ; };
term . onData ( d => {
if ( ws && ws . readyState === WebSocket . OPEN ) ws . send ( d );
});
term . onResize ( size => {
if ( ws && ws . readyState === WebSocket . OPEN ) ws . send ( `__resize__: ${ size . cols } : ${ size . rows } ` );
});
document . getElementById ( 'view-term' ). querySelector ( '#termWrap' ). style . height = '480px' ;
}
// ---------- submit ----------
2026-09-23 16:18:11 +08:00
async function submitFlag () {
const res = document . getElementById ( 'result' );
res . className = '' ;
const d = await api ( '/api/flag/submit' , {
method : 'POST' , headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ({
team : TEAM_ID ,
2026-09-23 18:05:44 +08:00
target : parseInt ( document . getElementById ( 'target' ). value || '0' , 10 ) || TEAM_ID ,
2026-09-23 16:18:11 +08:00
challenge : document . getElementById ( 'challenge' ). value ,
flag : document . getElementById ( 'flag' ). value . trim (),
}),
});
2026-09-23 16:37:58 +08:00
if ( d . success ) { res . className = 'ok' ; res . textContent = '✅ Flag benar! Poin masuk.' ; }
else { res . className = 'err' ; res . textContent = '❌ ' + ( d . error || 'Gagal' ); }
2026-09-23 16:18:11 +08:00
}
2026-09-23 18:05:44 +08:00
// target dropdown: all enemy teams (exclude own)
async function loadTargetDropdown () {
try {
const d = await api ( `/api/team/ ${ TEAM_ID } /targets` );
const sel = document . getElementById ( 'target' );
const targets = ( d . targets || []). filter ( t => t . team_idx !== TEAM_ID );
const seen = new Map ();
for ( const t of targets ) seen . set ( t . team_idx , t . team_label || ( 'Team ' + t . team_idx ));
sel . innerHTML = '' ;
if ( ! seen . size ) {
sel . innerHTML = '<option value="">Tidak ada tim musuh</option>' ;
return ;
}
for ( const [ idx , label ] of seen ) {
const o = document . createElement ( 'option' );
o . value = idx ; o . textContent = ` ${ label } (tim ${ idx } )` ;
sel . appendChild ( o );
}
} catch ( e ) { /* non-fatal */ }
}
2026-09-23 22:56:07 +08:00
// ---------- 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>' ;
}
}
2026-09-24 00:49:05 +08:00
// ---------- 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 );
2026-09-23 22:56:07 +08:00
// ---------- 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>
2026-09-24 00:49:05 +08:00
<span class="lb-solves">⚑ ${ t . solves } solve ${ t . solves > 1 ? 's' : '' } · 🏆 ${ t . points ?? 0 } pts</span>
2026-09-23 22:56:07 +08:00
</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 );
2026-09-23 18:05:44 +08:00
loadTargetDropdown ();
2026-09-23 16:37:58 +08:00
checkSession ();
2026-09-23 16:18:11 +08:00
</ script >
</ body >
</ html >