LineUp-Bot / dashboard.html
Brozy123's picture
Update dashboard.html
0912fe9 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FriendlyBot — Dashboard</title>
<link rel="icon" href="/logo.png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
:root {
--bg: #0a0a1a;
--surface: #12122a;
--surface2: #1a1a3a;
--border: rgba(255,255,255,0.06);
--accent: #5865F2;
--accent-glow: rgba(88,101,242,0.3);
--green: #57F287;
--red: #ED4245;
--yellow: #FEE75C;
--text: #ffffff;
--text-dim: rgba(255,255,255,0.5);
--text-mid: rgba(255,255,255,0.7);
--radius: 16px;
--radius-sm: 10px;
}
body {
font-family: 'Inter', -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
overflow-x: hidden;
}
/* ===== LOGIN ===== */
#login-screen {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: radial-gradient(ellipse at 50% 0%, rgba(88,101,242,0.15) 0%, transparent 60%);
}
.login-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 48px 40px;
width: 420px;
max-width: 90vw;
text-align: center;
backdrop-filter: blur(20px);
box-shadow: 0 24px 80px rgba(0,0,0,0.5);
animation: fadeUp 0.6s ease;
}
@keyframes fadeUp {
from { opacity:0; transform:translateY(30px); }
to { opacity:1; transform:translateY(0); }
}
.login-card img { width: 80px; border-radius: 50%; margin-bottom: 20px; box-shadow: 0 0 40px var(--accent-glow); }
.login-card h1 { font-size: 24px; font-weight: 700; margin-bottom: 6px; }
.login-card p { color: var(--text-dim); font-size: 14px; margin-bottom: 28px; }
.login-card input {
width: 100%;
padding: 14px 18px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
font-size: 15px;
font-family: 'Inter', sans-serif;
outline: none;
transition: border 0.2s;
margin-bottom: 14px;
}
.login-card input:focus { border-color: var(--accent); }
.login-card button {
width: 100%;
padding: 14px;
background: var(--accent);
color: white;
border: none;
border-radius: var(--radius-sm);
font-size: 15px;
font-weight: 600;
font-family: 'Inter', sans-serif;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.login-card button:hover { background: #4752c4; transform: translateY(-1px); box-shadow: 0 8px 30px var(--accent-glow); }
.login-card button svg { width: 20px; height: 20px; }
.login-error { color: var(--red); font-size: 13px; margin-top: 14px; display: none; }
.login-info { color: var(--text-dim); font-size: 12px; margin-top: 16px; line-height: 1.6; }
.user-pill {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
background: var(--surface2);
border-radius: 100px;
}
.user-pill img { width: 28px; height: 28px; border-radius: 50%; }
.user-pill span { font-size: 13px; font-weight: 600; }
/* ===== DASHBOARD ===== */
#dashboard { display: none; }
.sidebar {
position: fixed;
top: 0; left: 0;
width: 260px;
height: 100vh;
background: var(--surface);
border-right: 1px solid var(--border);
padding: 24px 16px;
z-index: 100;
display: flex;
flex-direction: column;
}
.sidebar-brand {
display: flex;
align-items: center;
gap: 12px;
padding: 0 8px 24px;
border-bottom: 1px solid var(--border);
margin-bottom: 20px;
}
.sidebar-brand img { width: 40px; border-radius: 50%; }
.sidebar-brand span { font-weight: 700; font-size: 17px; }
.sidebar-nav { flex: 1; display: flex; flex-direction: column; gap: 4px; }
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-radius: var(--radius-sm);
color: var(--text-mid);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
text-decoration: none;
}
.nav-item:hover { background: var(--surface2); color: var(--text); }
.nav-item.active { background: var(--accent); color: white; }
.nav-item svg { width: 20px; height: 20px; flex-shrink: 0; }
.sidebar-footer {
padding: 16px 8px 0;
border-top: 1px solid var(--border);
}
.sidebar-footer .nav-item { color: var(--red); }
.sidebar-footer .nav-item:hover { background: rgba(237,66,69,0.1); }
.main { margin-left: 260px; padding: 32px 40px; min-height: 100vh; }
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.topbar h2 { font-size: 26px; font-weight: 700; }
.topbar-right { display: flex; align-items: center; gap: 12px; }
.status-pill {
display: flex;
align-items: center;
gap: 8px;
background: rgba(87,242,135,0.1);
color: var(--green);
padding: 8px 16px;
border-radius: 100px;
font-size: 13px;
font-weight: 600;
}
.status-dot { width: 8px; height: 8px; background: var(--green); border-radius: 50%; animation: pulse 2s infinite; }
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
/* Stats Cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
.stat-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
position: relative;
overflow: hidden;
transition: all 0.25s;
}
.stat-card:hover { border-color: rgba(88,101,242,0.3); transform: translateY(-2px); box-shadow: 0 12px 40px rgba(0,0,0,0.3); }
.stat-card::after { content:''; position:absolute; top:0; right:0; width:120px; height:120px; background: radial-gradient(circle, var(--accent-glow) 0%, transparent 70%); opacity:0.15; pointer-events: none; }
.stat-card .label { font-size: 13px; color: var(--text-dim); font-weight: 500; text-transform: uppercase; letter-spacing: 1px; }
.stat-card .value { font-size: 36px; font-weight: 800; margin-top: 8px; line-height: 1; }
.stat-card .sub { font-size: 12px; color: var(--text-dim); margin-top: 8px; }
/* Panels */
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
margin-bottom: 24px;
}
.panel-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-header h3 { font-size: 16px; font-weight: 600; }
.panel-body { padding: 0; }
/* Server List */
.server-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-bottom: 1px solid var(--border);
transition: background 0.15s;
}
.server-row:last-child { border-bottom: none; }
.server-row:hover { background: var(--surface2); }
.server-info { display: flex; align-items: center; gap: 14px; }
.server-icon {
width: 40px; height: 40px;
border-radius: 50%;
background: var(--accent);
display: flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 16px; color: white; flex-shrink: 0;
}
.server-name { font-weight: 600; font-size: 14px; }
.server-members { color: var(--text-dim); font-size: 12px; margin-top: 2px; }
.server-rank { font-size: 13px; color: var(--text-dim); font-weight: 600; min-width: 30px; }
.member-badge {
background: var(--surface2);
padding: 6px 14px;
border-radius: 100px;
font-size: 13px;
font-weight: 500;
color: var(--text-mid);
}
/* Command List */
.cmd-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 24px;
border-bottom: 1px solid var(--border);
}
.cmd-row:last-child { border-bottom: none; }
.cmd-row:hover { background: var(--surface2); }
.cmd-name { font-family: 'Courier New', monospace; font-weight: 600; color: var(--accent); font-size: 14px; }
.cmd-desc { color: var(--text-dim); font-size: 13px; margin-top: 2px; }
.cmd-badge {
padding: 4px 12px;
border-radius: 100px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.cmd-badge.admin { background: rgba(237,66,69,0.15); color: var(--red); }
.cmd-badge.public { background: rgba(87,242,135,0.15); color: var(--green); }
.cmd-badge.staff { background: rgba(254,231,92,0.15); color: var(--yellow); }
/* Pages */
.page { display: none; }
.page.active { display: block; }
/* Invite Section */
.invite-box {
background: linear-gradient(135deg, #5865F2 0%, #4752c4 100%);
border-radius: var(--radius);
padding: 40px;
text-align: center;
margin-bottom: 24px;
}
.invite-box h3 { font-size: 22px; font-weight: 700; margin-bottom: 10px; }
.invite-box p { color: rgba(255,255,255,0.8); margin-bottom: 24px; font-size: 15px; }
.invite-btn {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 14px 32px;
background: white;
color: var(--accent);
border: none;
border-radius: var(--radius-sm);
font-size: 15px;
font-weight: 700;
font-family: 'Inter', sans-serif;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.invite-btn:hover { transform: translateY(-2px); box-shadow: 0 12px 40px rgba(0,0,0,0.3); }
/* Quick Actions */
.actions-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; padding: 24px; }
.action-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 20px;
cursor: pointer;
transition: all 0.2s;
}
.action-card:hover { border-color: var(--accent); transform: translateY(-2px); }
.action-card h4 { font-size: 14px; font-weight: 600; margin-bottom: 6px; }
.action-card p { font-size: 12px; color: var(--text-dim); }
.action-icon { font-size: 24px; margin-bottom: 12px; }
/* Logs */
.log-area {
background: var(--bg);
padding: 20px 24px;
font-family: 'Courier New', monospace;
font-size: 13px;
color: var(--green);
max-height: 400px;
overflow-y: auto;
line-height: 1.8;
}
.log-line { opacity: 0.85; }
.log-line.error { color: var(--red); }
.log-line.warn { color: var(--yellow); }
/* Responsive */
@media (max-width: 768px) {
.sidebar { display: none; }
.main { margin-left: 0; padding: 20px; }
}
/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }
/* Loading shimmer */
.shimmer {
background: linear-gradient(90deg, var(--surface) 25%, var(--surface2) 50%, var(--surface) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
/* Switch Toggle */
.switch {
position: relative;
display: inline-block;
width: 48px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: var(--surface2);
transition: .4s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 24px;
border: 1px solid var(--border);
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 2px;
bottom: 2px;
background-color: white;
transition: .4s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
input:checked + .slider {
background-color: var(--accent);
border-color: transparent;
box-shadow: 0 0 15px var(--accent-glow);
}
input:checked + .slider:before {
transform: translateX(24px);
}
/* Refresh Button */
.refresh-btn {
background: var(--surface2);
border: 1px solid var(--border);
color: var(--text-mid);
padding: 8px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8px;
}
.refresh-btn:hover {
color: var(--text);
border-color: var(--accent);
background: var(--surface);
transform: translateY(-1px);
}
.refresh-btn svg { width: 18px; height: 18px; }
.refresh-btn.loading svg {
animation: spin 0.8s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<!-- ===== LOGIN SCREEN ===== -->
<div id="login-screen">
<div class="login-card">
<img src="/logo.png" alt="FriendlyBot">
<h1>FriendlyBot</h1>
<p>Sign in with your Discord account to access the dashboard</p>
<button onclick="window.location.href='/auth/login'">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.73 4.87a18.2 18.2 0 0 0-4.6-1.44c-.21.4-.4.8-.58 1.21-1.69-.25-3.4-.25-5.1 0-.18-.4-.37-.8-.58-1.21-1.64.4-3.2 0.88-4.6 1.44C1.3 9.4 0.2 13.9 0.7 18.3a18.4 18.4 0 0 0 5.5 2.8c.4-.6.8-1.2 1.1-1.8-1.9-.7-3.7-1.6-5.4-2.8.4.3.9.6 1.3.9 3.5 2.1 7.4 2.1 10.9 0 .4-.3.9-.6 1.3-.9-1.7 1.2-3.5 2.1-5.4 2.8.3.6.7 1.2 1.1 1.8a18.4 18.4 0 0 0 5.5-2.8c.5-4.4-.6-8.9-3.7-13.43zM8.02 15.33c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm7.96 0c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/></svg>
Login with Discord
</button>
<div class="login-error" id="login-error"></div>
<div class="login-info">Server Administrators can access the dashboard to configure bot settings for their respective guilds.</div>
</div>
</div>
<!-- ===== DASHBOARD ===== -->
<div id="dashboard">
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-brand">
<img src="/logo.png" alt="">
<span>FriendlyBot</span>
</div>
<div class="sidebar-nav">
<div class="nav-item active" onclick="showPage('overview', this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
Overview
</div>
<div class="nav-item" onclick="showPage('servers', this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
Servers
</div>
<div class="nav-item" onclick="showPage('commands', this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
Commands
</div>
<div class="nav-item" onclick="showPage('league', this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l3 6 6 1-4 4 1 6-6-3-6 3 1-6-4-4 6-1z"/></svg>
League
</div>
<div class="nav-item" onclick="showPage('invite', this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg>
Invite Bot
</div>
<a class="nav-item" href="/premium" aria-label="Premium" style="color:var(--gold, #FFD700);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
Premium
</a>
</div>
</div>
<div class="sidebar-footer">
<div class="nav-item" onclick="logout()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
Sign Out
</div>
</div>
</div>
<!-- Main Content -->
<div class="main">
<!-- ===== OVERVIEW PAGE ===== -->
<div class="page active" id="page-overview">
<div class="topbar">
<h2>Dashboard</h2>
<div class="topbar-right">
<button class="refresh-btn" onclick="refreshAll()" title="Refresh Data">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
</button>
<div class="user-pill" id="user-pill" style="display:none">
<img id="user-avatar" src="" alt="">
<span id="user-name"></span>
</div>
<div class="status-pill"><div class="status-dot"></div> Online</div>
</div>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="label">Servers</div>
<div class="value" id="stat-servers"></div>
<div class="sub">Connected guilds</div>
</div>
<div class="stat-card">
<div class="label">Total Members</div>
<div class="value" id="stat-members"></div>
<div class="sub">Across all servers</div>
</div>
<div class="stat-card">
<div class="label">Uptime</div>
<div class="value" id="stat-uptime"></div>
<div class="sub">Since last restart</div>
</div>
<div class="stat-card">
<div class="label">Commands</div>
<div class="value" id="stat-commands"></div>
<div class="sub">Registered globally</div>
</div>
</div>
<div class="panel">
<div class="panel-header"><h3>⚡ Quick Actions</h3></div>
<div class="actions-grid">
<div class="action-card" onclick="window.open('/api/invite','_blank')">
<div class="action-icon">🤖</div>
<h4>Add to Server</h4>
<p>Generate a fresh invite link and add the bot to a new server.</p>
</div>
</div>
</div>
</div>
<!-- ===== SERVERS PAGE ===== -->
<div class="page" id="page-servers">
<div class="topbar">
<h2>Server Registry</h2>
<div class="topbar-right">
<div class="status-pill"><div class="status-dot"></div> <span id="server-count-label">0 Servers</span></div>
</div>
</div>
<div class="panel">
<div class="panel-header"><h3>🌐 All Connected Servers</h3><span style="color:var(--text-dim);font-size:13px">Sorted by members</span></div>
<div class="panel-body" id="server-list">
<div class="server-row shimmer" style="height:72px"></div>
<div class="server-row shimmer" style="height:72px"></div>
<div class="server-row shimmer" style="height:72px"></div>
</div>
</div>
</div>
<!-- ===== COMMANDS PAGE ===== -->
<div class="page" id="page-commands">
<div class="topbar"><h2>Command Registry</h2></div>
<div class="panel">
<div class="panel-header"><h3>📋 All Registered Commands</h3></div>
<div class="panel-body" id="command-list"></div>
</div>
</div>
<!-- ===== INVITE PAGE ===== -->
<div class="page" id="page-invite">
<div class="topbar"><h2>Invite Bot</h2></div>
<div class="invite-box">
<h3>Add FriendlyBot to Your Server</h3>
<p>Give your server the ultimate match management experience with lineup boards, formations, match tracking, and more.</p>
<a class="invite-btn" id="invite-link" href="#" target="_blank">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg>
Invite to Discord
</a>
</div>
<div class="panel">
<div class="panel-header"><h3>🔒 Required Permissions</h3></div>
<div class="actions-grid">
<div class="action-card" style="cursor:default">
<div class="action-icon">💬</div>
<h4>Send Messages</h4>
<p>Post lineup boards, match polls, and announcements.</p>
</div>
<div class="action-card" style="cursor:default">
<div class="action-icon">👤</div>
<h4>Manage Roles</h4>
<p>Assign and manage staff whitelist roles.</p>
</div>
<div class="action-card" style="cursor:default">
<div class="action-icon">📌</div>
<h4>Manage Messages</h4>
<p>Clear messages, pin important updates, bulk delete.</p>
</div>
<div class="action-card" style="cursor:default">
<div class="action-icon">🔒</div>
<h4>Manage Channels</h4>
<p>Lock/unlock channels during matches or events.</p>
</div>
</div>
</div>
</div>
<!-- ===== LEAGUE PAGE ===== -->
<div class="page" id="page-league">
<div class="topbar"><h2>League Center</h2></div>
<div class="invite-box" style="background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);">
<h3 style="color: #000;">Join the Official League</h3>
<p style="color: rgba(0,0,0,0.8);">Compete against the best teams and players. Winner receives exclusive premium features for FriendlyBot!</p>
<a class="invite-btn" href="https://discord.gg/vpHd4Xfm" target="_blank" style="color: #5865F2;">
<svg viewBox="0 0 24 24" fill="currentColor" style="width:20px; height:20px;"><path d="M19.73 4.87a18.2 18.2 0 0 0-4.6-1.44c-.21.4-.4.8-.58 1.21-1.69-.25-3.4-.25-5.1 0-.18-.4-.37-.8-.58-1.21-1.64.4-3.2 0.88-4.6 1.44C1.3 9.4 0.2 13.9 0.7 18.3a18.4 18.4 0 0 0 5.5 2.8c.4-.6.8-1.2 1.1-1.8-1.9-.7-3.7-1.6-5.4-2.8.4.3.9.6 1.3.9 3.5 2.1 7.4 2.1 10.9 0 .4-.3.9-.6 1.3-.9-1.7 1.2-3.5 2.1-5.4 2.8.3.6.7 1.2 1.1 1.8a18.4 18.4 0 0 0 5.5-2.8c.5-4.4-.6-8.9-3.7-13.43zM8.02 15.33c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm7.96 0c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/></svg>
Join Discord League
</a>
</div>
<div class="panel">
<div class="panel-header"><h3>🏆 League Info</h3></div>
<div class="panel-body" style="padding: 24px;">
<p style="margin-bottom: 20px; color: var(--text-mid); line-height: 1.6;">The FriendlyBot League is the premier competition for teams using our bot. Showcase your skills, climb the rankings, and earn special rewards.</p>
<div class="actions-grid" style="padding: 0; margin-bottom: 30px;">
<div class="action-card" style="cursor:default">
<div class="action-icon">💎</div>
<h4>Premium Features</h4>
<p>Winners gain access to exclusive bot features, custom themes, and advanced analytics.</p>
</div>
<div class="action-card" style="cursor:default">
<div class="action-icon">📅</div>
<h4>Weekly Matches</h4>
<p>Scheduled matches every weekend with full stat tracking and automated lineups.</p>
</div>
</div>
<div style="text-align: center; padding: 20px; border-top: 1px solid var(--border);">
<a href="/league" class="invite-btn" style="background: var(--accent); color: white;">
Visit Full League Website
</a>
<p style="font-size: 12px; color: var(--text-dim); margin-top: 12px;">View teams, rosters, and full standings on the official league page.</p>
</div>
</div>
</div>
</div>
<!-- ===== SERVER SETTINGS PAGE ===== -->
<div class="page" id="page-server-settings">
<div class="topbar">
<h2 id="settings-guild-name">Server Settings</h2>
<button class="invite-btn" onclick="showPage('servers')" style="padding:8px 16px; font-size:13px">Back to List</button>
</div>
<div class="panel">
<div class="panel-header"><h3>⚙️ General Configuration</h3></div>
<div class="panel-body" style="padding: 24px;">
<div style="margin-bottom: 32px;">
<label style="display:block; font-size:13px; color:var(--text-dim); margin-bottom:8px">Command Prefix</label>
<div style="display:flex; gap:10px">
<input type="text" id="config-prefix" placeholder="!" style="width:100px; padding:12px; background:var(--bg); border:1px solid var(--border); border-radius:var(--radius-sm); color:white; font-size:16px; outline:none">
</div>
<p style="font-size:12px; color:var(--text-dim); margin-top:10px">The prefix used to trigger message-based commands (e.g. ! or . or ?)</p>
</div>
<div style="border-top:1px solid var(--border); padding-top:32px; margin-bottom: 32px;">
<h4 style="margin-bottom:16px; font-size:15px; color:var(--accent)">🤖 Auto Friendly System</h4>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:24px">
<div>
<label style="display:block; font-size:13px; color:var(--text-dim); margin-bottom:8px">Feature Status</label>
<div style="display:flex; align-items:center; gap:12px">
<label class="switch">
<input type="checkbox" id="config-auto-enabled">
<span class="slider"></span>
</label>
<span style="font-size:14px; color:var(--text-mid)">Enabled</span>
</div>
</div>
<div>
<label style="display:block; font-size:13px; color:var(--text-dim); margin-bottom:8px">Interval (Minutes)</label>
<input type="number" id="config-auto-interval" placeholder="40" style="width:100%; padding:12px; background:var(--bg); border:1px solid var(--border); border-radius:var(--radius-sm); color:white; outline:none">
</div>
<div>
<label style="display:block; font-size:13px; color:var(--text-dim); margin-bottom:8px">Target Channel</label>
<select id="config-auto-channel" style="width:100%; padding:12px; background:var(--bg); border:1px solid var(--border); border-radius:var(--radius-sm); color:white; outline:none">
<option value="">Select a channel...</option>
</select>
</div>
</div>
<p style="font-size:12px; color:var(--text-dim); margin-top:16px">When enabled, the bot will automatically post a friendly match poll in the selected channel at the specified interval.</p>
</div>
<div style="border-top:1px solid var(--border); padding-top:32px; margin-bottom: 32px;">
<h4 style="margin-bottom:16px; font-size:15px; color:var(--accent)">📢 Broadcast & Announcements</h4>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:24px">
<div>
<label style="display:block; font-size:13px; color:var(--text-dim); margin-bottom:8px">Receive Broadcasts</label>
<div style="display:flex; align-items:center; gap:12px">
<label class="switch">
<input type="checkbox" id="config-post-enabled">
<span class="slider"></span>
</label>
<span style="font-size:14px; color:var(--text-mid)">Enabled</span>
</div>
</div>
<div>
<label style="display:block; font-size:13px; color:var(--text-dim); margin-bottom:8px">Broadcast Channel</label>
<select id="config-post-channel" style="width:100%; padding:12px; background:var(--bg); border:1px solid var(--border); border-radius:var(--radius-sm); color:white; outline:none">
<option value="">Default (System Channel)</option>
</select>
</div>
</div>
<p style="font-size:12px; color:var(--text-dim); margin-top:16px">Control where global announcements from the bot owner are posted. You can opt-out by disabling this feature.</p>
</div>
<div style="border-top:1px solid var(--border); padding-top:32px; margin-bottom: 32px;">
<h4 style="margin-bottom:16px; font-size:15px; color:var(--accent)">🧹 Auto Cleanup & Maintenance</h4>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:24px">
<div>
<label style="display:block; font-size:13px; color:var(--text-dim); margin-bottom:8px">Poll Cleanup (Hours)</label>
<input type="number" id="config-cleanup-hours" placeholder="2" style="width:100%; padding:12px; background:var(--bg); border:1px solid var(--border); border-radius:var(--radius-sm); color:white; outline:none">
</div>
<div>
<label style="display:block; font-size:13px; color:var(--text-dim); margin-bottom:8px">Lineup Cleanup (Hours)</label>
<input type="number" id="config-lineup-cleanup-hours" placeholder="2" style="width:100%; padding:12px; background:var(--bg); border:1px solid var(--border); border-radius:var(--radius-sm); color:white; outline:none">
</div>
</div>
<p style="font-size:12px; color:var(--text-dim); margin-top:16px">The bot will automatically delete friendly polls and lineup boards after the specified time to keep channels clean.</p>
</div>
<div style="border-top:1px solid var(--border); padding-top:32px; margin-bottom: 32px;">
<h4 style="margin-bottom:16px; font-size:15px; color:var(--accent)">⏰ Match Reminders</h4>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:24px">
<div>
<label style="display:block; font-size:13px; color:var(--text-dim); margin-bottom:8px">Enable DM Reminders</label>
<div style="display:flex; align-items:center; gap:12px">
<label class="switch">
<input type="checkbox" id="config-reminders-enabled">
<span class="slider"></span>
</label>
<span style="font-size:14px; color:var(--text-mid)">Enabled</span>
</div>
</div>
<div>
<label style="display:block; font-size:13px; color:var(--text-dim); margin-bottom:8px">Lead Time (Minutes)</label>
<input type="number" id="config-reminders-lead" placeholder="30" style="width:100%; padding:12px; background:var(--bg); border:1px solid var(--border); border-radius:var(--radius-sm); color:white; outline:none">
</div>
</div>
<p style="font-size:12px; color:var(--text-dim); margin-top:16px">When enabled, players on the lineup board will receive a DM reminder before a scheduled league match starts.</p>
</div>
<div style="border-top:1px solid var(--border); padding-top:32px; margin-bottom: 32px;">
<h4 style="margin-bottom:16px; font-size:15px; color:var(--accent)">💬 Command Messages</h4>
<div style="margin-bottom: 24px;">
<label style="display:block; font-size:13px; color:var(--text-dim); margin-bottom:8px">Friendly Command Text</label>
<textarea id="config-friendly-text" oninput="autoResize(this)" style="width:100%; min-height:100px; padding:12px; background:var(--bg); border:1px solid var(--border); border-radius:var(--radius-sm); color:white; font-family:monospace; font-size:13px; outline:none; resize:none; overflow:hidden; transition: height 0.2s ease;"></textarea>
<p style="font-size:12px; color:var(--text-dim); margin-top:8px">Use <code>{needed}</code> as a placeholder for the number of players needed.</p>
</div>
</div>
<div style="border-top:1px solid var(--border); padding-top:24px; text-align:right">
<button class="invite-btn" onclick="saveConfig()" style="background:var(--accent); color:white; padding:12px 32px">Save All Changes</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// ===== AUTH (Discord OAuth2 via httpOnly cookie) =====
let currentUser = null;
document.addEventListener('DOMContentLoaded', () => {
// Check URL params for errors
const params = new URLSearchParams(window.location.search);
if (params.get('error') === 'not_owner') {
document.getElementById('login-error').textContent = '❌ Access denied. You must be an Administrator or have Manage Server permissions to access the dashboard.';
document.getElementById('login-error').style.display = 'block';
} else if (params.get('error')) {
document.getElementById('login-error').textContent = '❌ Login failed. Please try again.';
document.getElementById('login-error').style.display = 'block';
}
// Check for existing session
fetch('/api/me')
.then(r => r.json())
.then(data => {
if (data.loggedIn) {
currentUser = data.user;
showDashboard();
// Clean URL
if (params.has('login') || params.has('error')) {
window.history.replaceState({}, '', '/');
}
}
})
.catch(() => {});
});
function showDashboard() {
document.getElementById('login-screen').style.display = 'none';
document.getElementById('dashboard').style.display = 'block';
// Show user info
if (currentUser) {
document.getElementById('user-avatar').src = currentUser.avatar;
document.getElementById('user-name').textContent = currentUser.username;
document.getElementById('user-pill').style.display = 'flex';
}
loadStats();
loadServers();
loadCommands();
loadLogs();
// Regular intervals
setInterval(loadStats, 15000);
setInterval(loadServers, 15000);
setInterval(loadLogs, 10000);
// Auto-refresh when tab becomes visible or focused
// This solves the issue of not seeing a server after adding it
window.addEventListener('focus', () => {
console.log("Tab focused, refreshing dashboard data...");
refreshAll();
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
refreshAll();
}
});
}
function refreshAll() {
const btn = document.querySelector('.refresh-btn');
if (btn) btn.classList.add('loading');
return Promise.all([
loadStats(),
loadServers(),
loadLogs()
]).finally(() => {
setTimeout(() => {
if (btn) btn.classList.remove('loading');
}, 600);
});
}
function logout() {
window.location.href = '/auth/logout';
}
// ===== NAVIGATION =====
function showPage(name, el) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById('page-' + name).classList.add('active');
if (el) el.classList.add('active');
}
// ===== DATA LOADERS =====
function loadStats() {
return fetch('/api/stats')
.then(r => r.json())
.then(data => {
document.getElementById('stat-servers').textContent = data.servers || 0;
document.getElementById('stat-members').textContent = (data.members || 0).toLocaleString();
document.getElementById('stat-uptime').textContent = data.uptime || '—';
document.getElementById('stat-commands').textContent = data.commands || 0;
document.getElementById('server-count-label').textContent = (data.servers || 0) + ' Servers';
if (data.inviteUrl) document.getElementById('invite-link').href = data.inviteUrl;
}).catch(() => {});
}
function loadServers() {
return fetch('/api/servers')
.then(r => r.json())
.then(data => {
const servers = Array.isArray(data) ? data : (Array.isArray(data.servers) ? data.servers : []);
const container = document.getElementById('server-list');
container.innerHTML = '';
if (data && data.pending) {
container.innerHTML = '<div class="server-row"><div class="server-info"><div><div class="server-name">Connecting to Discord...</div><div class="server-members">Bot is still starting up on Hugging Face. This list will auto-refresh.</div></div></div></div>';
return;
}
if (!servers.length) {
container.innerHTML = '<div class="server-row"><div class="server-info"><div><div class="server-name">No servers found</div><div class="server-members">Make sure the bot is in your server and your account has Manage Server or Administrator permissions.</div></div></div></div>';
return;
}
servers.sort((a, b) => (b.members || 0) - (a.members || 0));
servers.forEach((s, i) => {
const iconHtml = s.icon
? `<img class="server-icon" src="${s.icon}" style="object-fit:cover">`
: `<div class="server-icon">${s.name.charAt(0).toUpperCase()}</div>`;
container.innerHTML += `
<div class="server-row" onclick="openServerSettings('${s.id}', '${s.name.replace(/'/g, "\\'")}')" style="cursor:pointer">
<div class="server-info">
<span class="server-rank">#${i + 1}</span>
${iconHtml}
<div>
<div class="server-name">${escapeHtml(s.name)}</div>
<div class="server-members">${s.id}</div>
</div>
</div>
<div style="display:flex; align-items:center; gap:12px">
<div class="member-badge">${(s.members || 0).toLocaleString()} members</div>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="opacity:0.3"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>`;
});
}).catch(() => {
const container = document.getElementById('server-list');
container.innerHTML = '<div class="server-row"><div class="server-info"><div><div class="server-name">Unable to load servers</div><div class="server-members">Please refresh the page and try again.</div></div></div></div>';
});
}
let activeGuildId = null;
function openServerSettings(id, name) {
activeGuildId = id;
document.getElementById('settings-guild-name').textContent = 'Settings: ' + name;
showPage('server-settings');
// Load current config
fetch('/api/config/' + id)
.then(r => r.json())
.then(data => {
document.getElementById('config-prefix').value = data.prefix || '!';
document.getElementById('config-auto-enabled').checked = !!data.autoFriendlyEnabled;
document.getElementById('config-auto-interval').value = data.autoFriendlyInterval || 40;
document.getElementById('config-post-enabled').checked = data.postEnabled !== false;
document.getElementById('config-friendly-text').value = data.friendlyText || "";
// Auto-resize the textarea after setting value
const friendlyTextarea = document.getElementById('config-friendly-text');
setTimeout(() => autoResize(friendlyTextarea), 50);
document.getElementById('config-cleanup-hours').value = data.cleanupHours || 2;
document.getElementById('config-lineup-cleanup-hours').value = data.lineupCleanupHours || 2;
document.getElementById('config-reminders-enabled').checked = !!data.remindersEnabled;
document.getElementById('config-reminders-lead').value = data.remindersLead || 30;
// Load channels and set selected
fetch('/api/channels/' + id)
.then(cr => cr.json())
.then(channels => {
const autoSelect = document.getElementById('config-auto-channel');
const postSelect = document.getElementById('config-post-channel');
autoSelect.innerHTML = '<option value="">Select a channel...</option>';
postSelect.innerHTML = '<option value="">Default (System Channel)</option>';
channels.forEach(c => {
const optA = document.createElement('option');
optA.value = c.id;
optA.textContent = '#' + c.name;
if (c.id === data.autoFriendlyChannel) optA.selected = true;
autoSelect.appendChild(optA);
const optP = document.createElement('option');
optP.value = c.id;
optP.textContent = '#' + c.name;
if (c.id === data.postChannelId) optP.selected = true;
postSelect.appendChild(optP);
});
});
});
}
function saveConfig() {
if (!activeGuildId) return;
const prefix = document.getElementById('config-prefix').value;
const autoFriendlyEnabled = document.getElementById('config-auto-enabled').checked;
const autoFriendlyInterval = document.getElementById('config-auto-interval').value;
const autoFriendlyChannel = document.getElementById('config-auto-channel').value;
const postEnabled = document.getElementById('config-post-enabled').checked;
const postChannelId = document.getElementById('config-post-channel').value;
const friendlyText = document.getElementById('config-friendly-text').value;
const cleanupHours = document.getElementById('config-cleanup-hours').value;
const lineupCleanupHours = document.getElementById('config-lineup-cleanup-hours').value;
const remindersEnabled = document.getElementById('config-reminders-enabled').checked;
const remindersLead = document.getElementById('config-reminders-lead').value;
fetch('/api/config/' + activeGuildId, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prefix,
autoFriendlyEnabled,
autoFriendlyInterval,
autoFriendlyChannel,
postEnabled,
postChannelId,
friendlyText,
cleanupHours,
lineupCleanupHours,
remindersEnabled,
remindersLead
})
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('✅ Settings saved successfully!');
} else {
alert('❌ Error: ' + data.error);
}
}).catch(() => alert('❌ Network error saving settings.'));
}
function loadCommands() {
return fetch('/api/commands')
.then(r => r.json())
.then(data => {
const container = document.getElementById('command-list');
container.innerHTML = '';
data.forEach(c => {
const badge = c.admin ? '<span class="cmd-badge admin">Admin</span>'
: c.staff ? '<span class="cmd-badge staff">Staff</span>'
: '<span class="cmd-badge public">Public</span>';
container.innerHTML += `
<div class="cmd-row">
<div>
<div class="cmd-name">/${c.name}</div>
<div class="cmd-desc">${escapeHtml(c.description)}</div>
</div>
${badge}
</div>`;
});
}).catch(() => {});
}
function loadLogs() {
return fetch('/api/logs')
.then(r => r.json())
.then(data => {
const area = document.getElementById('log-area');
area.innerHTML = '';
(data.logs || []).forEach(line => {
const cls = line.includes('Error') || line.includes('CRITICAL') ? 'error'
: line.includes('Warning') || line.includes('Deprecation') ? 'warn' : '';
area.innerHTML += `<div class="log-line ${cls}">${escapeHtml(line)}</div>`;
});
area.scrollTop = area.scrollHeight;
}).catch(() => {});
}
function triggerAction(action) {
fetch('/api/action/' + action, { method: 'POST' })
.then(r => r.json())
.then(data => {
alert(data.message || 'Action triggered!');
loadStats();
}).catch(() => alert('Failed to trigger action.'));
}
function escapeHtml(str) {
const d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px';
}
</script>
</body>
</html>