Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PhishGuard Admin</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); | |
| * { margin:0; padding:0; box-sizing:border-box; } | |
| :root { | |
| --bg: #0F0F14; --bg2: #1A1A24; --card: #22222E; --border: rgba(255,255,255,0.06); | |
| --text: #EAEAF0; --text2: #8888A0; --accent: #534AB7; | |
| --safe: #22C55E; --danger: #EF4444; --warn: #F59E0B; | |
| } | |
| body { font-family:'Inter',sans-serif; background:var(--bg); color:var(--text); min-height:100vh; } | |
| /* Login */ | |
| .login-wrap { display:flex; align-items:center; justify-content:center; min-height:100vh; } | |
| .login-box { background:var(--card); padding:36px; border-radius:16px; border:1px solid var(--border); width:340px; } | |
| .login-box h2 { font-size:20px; margin-bottom:20px; text-align:center; } | |
| .login-box input { width:100%; padding:10px 14px; background:var(--bg2); border:1px solid var(--border); | |
| border-radius:8px; color:var(--text); font-size:14px; font-family:inherit; margin-bottom:12px; outline:none; } | |
| .login-box input:focus { border-color:var(--accent); } | |
| .login-box button { width:100%; padding:10px; background:linear-gradient(135deg,var(--accent),#6C5ECE); | |
| border:none; border-radius:8px; color:#fff; font-size:14px; font-weight:600; cursor:pointer; font-family:inherit; } | |
| .login-box button:hover { opacity:0.9; } | |
| .login-error { color:var(--danger); font-size:12px; text-align:center; margin-top:8px; display:none; } | |
| /* Dashboard */ | |
| .dashboard { display:none; max-width:1000px; margin:0 auto; padding:24px; } | |
| .dash-header { display:flex; align-items:center; gap:12px; margin-bottom:24px; } | |
| .dash-header h1 { font-size:22px; flex:1; } | |
| .dash-header h1 span { color:var(--accent); } | |
| .logout-btn { padding:6px 16px; background:var(--card); border:1px solid var(--border); | |
| border-radius:8px; color:var(--text2); font-size:12px; cursor:pointer; font-family:inherit; } | |
| /* Stats Cards */ | |
| .stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:12px; margin-bottom:24px; } | |
| .stat-card { background:var(--card); border:1px solid var(--border); border-radius:12px; padding:16px; } | |
| .stat-label { font-size:11px; color:var(--text2); text-transform:uppercase; letter-spacing:0.5px; } | |
| .stat-value { font-size:28px; font-weight:700; margin-top:4px; } | |
| .stat-sub { font-size:11px; color:var(--text2); margin-top:2px; } | |
| /* Table */ | |
| .section-title { font-size:16px; font-weight:600; margin-bottom:12px; } | |
| .table-wrap { background:var(--card); border:1px solid var(--border); border-radius:12px; overflow:hidden; margin-bottom:24px; } | |
| table { width:100%; border-collapse:collapse; font-size:12px; } | |
| th { background:var(--bg2); padding:10px 14px; text-align:left; font-weight:600; color:var(--text2); | |
| text-transform:uppercase; letter-spacing:0.5px; font-size:10px; } | |
| td { padding:10px 14px; border-top:1px solid var(--border); } | |
| tr:hover td { background:rgba(255,255,255,0.02); } | |
| .badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:10px; font-weight:600; } | |
| .badge-phish { background:rgba(239,68,68,0.12); color:var(--danger); } | |
| .badge-safe { background:rgba(34,197,94,0.12); color:var(--safe); } | |
| .url-cell { max-width:300px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-family:'SF Mono',monospace; font-size:11px; color:var(--text2); } | |
| /* Retrain Button */ | |
| .retrain-bar { display:flex; align-items:center; gap:12px; margin-bottom:24px; } | |
| .retrain-btn { padding:10px 24px; background:linear-gradient(135deg,var(--accent),#6C5ECE); | |
| border:none; border-radius:8px; color:#fff; font-size:13px; font-weight:600; cursor:pointer; font-family:inherit; } | |
| .retrain-btn:hover { opacity:0.9; } | |
| .retrain-btn:disabled { opacity:0.4; cursor:not-allowed; } | |
| .retrain-status { font-size:12px; color:var(--text2); } | |
| /* History */ | |
| .history-card { background:var(--card); border:1px solid var(--border); border-radius:12px; padding:16px; margin-bottom:8px; | |
| display:flex; align-items:center; gap:16px; } | |
| .hist-version { font-size:22px; font-weight:700; color:var(--accent); min-width:48px; text-align:center; } | |
| .hist-detail { flex:1; } | |
| .hist-detail div:first-child { font-size:13px; font-weight:500; } | |
| .hist-detail div:last-child { font-size:11px; color:var(--text2); margin-top:2px; } | |
| .hist-accuracy { font-size:14px; font-weight:600; } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Login Screen --> | |
| <div class="login-wrap" id="loginScreen"> | |
| <div class="login-box"> | |
| <h2>π‘οΈ PhishGuard Admin</h2> | |
| <input type="password" id="passInput" placeholder="Admin password" autocomplete="off"> | |
| <button onclick="attemptLogin()">Login</button> | |
| <div class="login-error" id="loginError">Invalid password</div> | |
| </div> | |
| </div> | |
| <!-- Dashboard (hidden until login) --> | |
| <div class="dashboard" id="dashboard"> | |
| <div class="dash-header"> | |
| <h1>Phish<span>Guard</span> Admin</h1> | |
| <button class="logout-btn" onclick="logout()">Logout</button> | |
| </div> | |
| <!-- Stats --> | |
| <div class="stats"> | |
| <div class="stat-card"> | |
| <div class="stat-label">Total Feedback</div> | |
| <div class="stat-value" id="sTotalFeedback">β</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Phishing Reports</div> | |
| <div class="stat-value" id="sPhishing" style="color:var(--danger)">β</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Safe Reports</div> | |
| <div class="stat-value" id="sSafe" style="color:var(--safe)">β</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Model Version</div> | |
| <div class="stat-value" id="sVersion" style="color:var(--accent)">β</div> | |
| <div class="stat-sub" id="sLastRetrain">Never retrained</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Unprocessed</div> | |
| <div class="stat-value" id="sUnprocessed" style="color:var(--warn)">β</div> | |
| <div class="stat-sub">of 50 needed</div> | |
| </div> | |
| </div> | |
| <!-- Manual Retrain --> | |
| <div class="retrain-bar"> | |
| <button class="retrain-btn" id="retrainBtn" onclick="triggerRetrain()">π Trigger Retraining</button> | |
| <div class="retrain-status" id="retrainStatus"></div> | |
| </div> | |
| <!-- Recent Feedback --> | |
| <div class="section-title">Recent Feedback</div> | |
| <div class="table-wrap"> | |
| <table> | |
| <thead> | |
| <tr><th>URL</th><th>Label</th><th>Source</th><th>Prediction</th><th>Time</th></tr> | |
| </thead> | |
| <tbody id="feedbackTable"><tr><td colspan="5" style="text-align:center;color:var(--text2)">Loading...</td></tr></tbody> | |
| </table> | |
| </div> | |
| <!-- Retrain History --> | |
| <div class="section-title">Retrain History</div> | |
| <div id="historyList"><div style="color:var(--text2);font-size:12px">Loading...</div></div> | |
| </div> | |
| <script> | |
| const BASE = window.location.origin; | |
| let authToken = ""; | |
| // Login | |
| function attemptLogin() { | |
| const pass = document.getElementById("passInput").value; | |
| fetch(`${BASE}/admin/login`, { | |
| method: "POST", | |
| headers: {"Content-Type":"application/json"}, | |
| body: JSON.stringify({password: pass}) | |
| }) | |
| .then(r => r.json()) | |
| .then(data => { | |
| if (data.success) { | |
| authToken = data.token; | |
| document.getElementById("loginScreen").style.display = "none"; | |
| document.getElementById("dashboard").style.display = "block"; | |
| loadDashboard(); | |
| } else { | |
| document.getElementById("loginError").style.display = "block"; | |
| } | |
| }) | |
| .catch(() => { document.getElementById("loginError").style.display = "block"; }); | |
| } | |
| document.getElementById("passInput").addEventListener("keyup", e => { if(e.key==="Enter") attemptLogin(); }); | |
| function logout() { | |
| authToken = ""; | |
| document.getElementById("loginScreen").style.display = "flex"; | |
| document.getElementById("dashboard").style.display = "none"; | |
| } | |
| // Dashboard data | |
| function loadDashboard() { | |
| // Stats | |
| fetch(`${BASE}/admin/data?token=${authToken}`).then(r=>r.json()).then(data => { | |
| if (data.error) { logout(); return; } | |
| const s = data.stats; | |
| document.getElementById("sTotalFeedback").textContent = s.total_feedback; | |
| document.getElementById("sPhishing").textContent = s.phishing_corrections; | |
| document.getElementById("sSafe").textContent = s.safe_corrections; | |
| document.getElementById("sVersion").textContent = "v" + s.model_version; | |
| document.getElementById("sUnprocessed").textContent = s.unprocessed_count; | |
| document.getElementById("sLastRetrain").textContent = s.last_retrain | |
| ? "Last: " + new Date(s.last_retrain).toLocaleString() : "Never retrained"; | |
| // Feedback table | |
| const rows = data.recent.map(e => ` | |
| <tr> | |
| <td class="url-cell" title="${esc(e.url)}">${esc(e.url)}</td> | |
| <td><span class="badge ${e.label==='phishing'?'badge-phish':'badge-safe'}">${esc(e.label)}</span></td> | |
| <td>${esc(e.source||'β')}</td> | |
| <td>${e.original_prediction!=null ? (e.original_prediction*100).toFixed(0)+'%' : 'β'}</td> | |
| <td style="font-size:11px;color:var(--text2)">${e.timestamp ? new Date(e.timestamp).toLocaleString() : 'β'}</td> | |
| </tr> | |
| `).join(""); | |
| document.getElementById("feedbackTable").innerHTML = rows || '<tr><td colspan="5" style="text-align:center;color:var(--text2)">No feedback yet</td></tr>'; | |
| // History | |
| const hist = (s.retrain_history || []).reverse(); | |
| if (hist.length === 0) { | |
| document.getElementById("historyList").innerHTML = '<div style="color:var(--text2);font-size:12px">No retraining history</div>'; | |
| } else { | |
| document.getElementById("historyList").innerHTML = hist.map(h => ` | |
| <div class="history-card"> | |
| <div class="hist-version">v${h.version}</div> | |
| <div class="hist-detail"> | |
| <div>Trained on ${h.samples} samples</div> | |
| <div>${new Date(h.timestamp).toLocaleString()}</div> | |
| </div> | |
| <div class="hist-accuracy" style="color:${h.accuracy>=0.8?'var(--safe)':h.accuracy>=0.6?'var(--warn)':'var(--danger)'}"> | |
| ${(h.accuracy*100).toFixed(1)}% | |
| </div> | |
| </div> | |
| `).join(""); | |
| } | |
| }); | |
| } | |
| // Retrain | |
| function triggerRetrain() { | |
| const btn = document.getElementById("retrainBtn"); | |
| btn.disabled = true; | |
| btn.textContent = "β³ Retraining..."; | |
| document.getElementById("retrainStatus").textContent = "Training in progress..."; | |
| fetch(`${BASE}/admin/retrain?token=${authToken}`, {method:"POST"}) | |
| .then(r=>r.json()) | |
| .then(data => { | |
| document.getElementById("retrainStatus").textContent = data.message || "Done"; | |
| btn.disabled = false; | |
| btn.textContent = "π Trigger Retraining"; | |
| setTimeout(loadDashboard, 2000); | |
| }) | |
| .catch(e => { | |
| document.getElementById("retrainStatus").textContent = "Error: " + e.message; | |
| btn.disabled = false; | |
| btn.textContent = "π Trigger Retraining"; | |
| }); | |
| } | |
| function esc(s) { const d=document.createElement('div'); d.textContent=String(s||''); return d.innerHTML; } | |
| // Auto-refresh every 30s | |
| setInterval(() => { if(authToken) loadDashboard(); }, 30000); | |
| </script> | |
| </body> | |
| </html> | |