AeThex-Bot-Master/aethex-bot/public/dashboard.html
sirpiglr cada20b646 Add analytics tracking and display for bot activity
Introduces new analytics tracking functions in `bot.js` for command usage, XP distribution, new members, and mod actions. Also adds an `/analytics` API endpoint to serve this data and corresponding CSS styling for the dashboard's analytics section in `dashboard.html`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 324e6a0e-a696-412d-aaf0-df4936017eb3
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/ocC7ZpF
Replit-Helium-Checkpoint-Created: true
2025-12-08 04:42:18 +00:00

2105 lines
75 KiB
HTML

<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AeThex Bot Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-primary: #0f0f23;
--bg-secondary: #1a1a2e;
--bg-tertiary: #16213e;
--bg-card: rgba(255,255,255,0.05);
--text-primary: #ffffff;
--text-secondary: #a1a1aa;
--text-muted: #71717a;
--accent: #7c3aed;
--accent-hover: #6d28d9;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--info: #3b82f6;
--border: rgba(255,255,255,0.1);
--sidebar-width: 260px;
}
[data-theme="light"] {
--bg-primary: #f8fafc;
--bg-secondary: #ffffff;
--bg-tertiary: #f1f5f9;
--bg-card: rgba(0,0,0,0.03);
--text-primary: #1e293b;
--text-secondary: #64748b;
--text-muted: #94a3b8;
--border: rgba(0,0,0,0.1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
display: flex;
}
/* Sidebar */
.sidebar {
width: var(--sidebar-width);
background: var(--bg-secondary);
border-right: 1px solid var(--border);
height: 100vh;
position: fixed;
left: 0;
top: 0;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
z-index: 100;
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 0.75rem;
}
.logo {
width: 40px;
height: 40px;
background: linear-gradient(135deg, var(--accent) 0%, #a855f7 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1.25rem;
}
.brand-name {
font-weight: 600;
font-size: 1.125rem;
}
.brand-tag {
font-size: 0.75rem;
color: var(--text-muted);
}
.nav-section {
padding: 1rem 0;
flex: 1;
overflow-y: auto;
}
.nav-label {
padding: 0.5rem 1.5rem;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-muted);
font-weight: 600;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1.5rem;
color: var(--text-secondary);
text-decoration: none;
cursor: pointer;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.nav-item:hover {
background: var(--bg-card);
color: var(--text-primary);
}
.nav-item.active {
background: rgba(124, 58, 237, 0.1);
color: var(--accent);
border-left-color: var(--accent);
}
.nav-item svg {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.nav-badge {
margin-left: auto;
background: var(--danger);
color: white;
font-size: 0.7rem;
padding: 0.125rem 0.5rem;
border-radius: 999px;
font-weight: 600;
}
.sidebar-footer {
padding: 1rem 1.5rem;
border-top: 1px solid var(--border);
}
/* Main Content */
.main-content {
margin-left: var(--sidebar-width);
flex: 1;
min-height: 100vh;
}
.topbar {
height: 64px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1.5rem;
position: sticky;
top: 0;
z-index: 50;
}
.topbar-left {
display: flex;
align-items: center;
gap: 1rem;
}
.menu-toggle {
display: none;
background: none;
border: none;
color: var(--text-primary);
cursor: pointer;
padding: 0.5rem;
}
.page-title {
font-size: 1.25rem;
font-weight: 600;
}
.topbar-right {
display: flex;
align-items: center;
gap: 1rem;
}
.status-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--bg-card);
border-radius: 999px;
font-size: 0.875rem;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
animation: pulse 2s infinite;
}
.status-dot.offline { background: var(--danger); animation: none; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.theme-toggle {
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-primary);
padding: 0.5rem;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.theme-toggle svg { width: 20px; height: 20px; }
/* Content Area */
.content {
padding: 1.5rem;
}
.section {
display: none;
}
.section.active {
display: block;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.25rem;
display: flex;
align-items: flex-start;
gap: 1rem;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.stat-icon.purple { background: rgba(124, 58, 237, 0.2); color: var(--accent); }
.stat-icon.green { background: rgba(16, 185, 129, 0.2); color: var(--success); }
.stat-icon.blue { background: rgba(59, 130, 246, 0.2); color: var(--info); }
.stat-icon.orange { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
.stat-icon.red { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
.stat-icon svg { width: 24px; height: 24px; }
.stat-info { flex: 1; }
.stat-label {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 0.25rem;
}
.stat-value {
font-size: 1.75rem;
font-weight: 700;
}
.stat-change {
font-size: 0.75rem;
margin-top: 0.25rem;
}
.stat-change.positive { color: var(--success); }
.stat-change.negative { color: var(--danger); }
/* Cards */
.card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
margin-bottom: 1.5rem;
}
.card-header {
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
.card-title {
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-title svg { width: 18px; height: 18px; color: var(--accent); }
.card-body {
padding: 1.25rem;
}
.card-actions {
display: flex;
gap: 0.5rem;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.btn svg { width: 16px; height: 16px; }
.btn-primary {
background: var(--accent);
color: white;
}
.btn-primary:hover { background: var(--accent-hover); }
.btn-secondary {
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid var(--border);
}
.btn-secondary:hover { background: var(--bg-tertiary); }
.btn-sm { padding: 0.375rem 0.75rem; font-size: 0.8rem; }
/* Tables */
.table-wrapper {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 0.875rem 1rem;
text-align: left;
}
th {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
font-weight: 600;
border-bottom: 1px solid var(--border);
}
td {
border-bottom: 1px solid var(--border);
font-size: 0.875rem;
}
tr:last-child td { border-bottom: none; }
tr:hover { background: var(--bg-card); }
/* Server list */
.server-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 0;
border-bottom: 1px solid var(--border);
}
.server-item:last-child { border-bottom: none; }
.server-icon {
width: 40px;
height: 40px;
border-radius: 10px;
background: var(--bg-tertiary);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
flex-shrink: 0;
}
.server-info { flex: 1; }
.server-name { font-weight: 500; }
.server-members {
font-size: 0.8rem;
color: var(--text-muted);
}
/* Activity Feed */
.activity-item {
display: flex;
gap: 0.75rem;
padding: 0.75rem 0;
border-bottom: 1px solid var(--border);
}
.activity-item:last-child { border-bottom: none; }
.activity-icon {
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.activity-icon svg { width: 16px; height: 16px; }
.activity-content { flex: 1; }
.activity-text { font-size: 0.875rem; }
.activity-time {
font-size: 0.75rem;
color: var(--text-muted);
}
/* Commands Grid */
.commands-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 0.75rem;
}
.command-item {
background: var(--bg-card);
padding: 0.75rem 1rem;
border-radius: 8px;
font-family: 'Monaco', 'Consolas', monospace;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--accent);
}
.command-item::before {
content: '/';
color: var(--text-muted);
}
/* Grid Layout */
.grid-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
.grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
/* Responsive */
@media (max-width: 1024px) {
.grid-3 { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
}
.main-content {
margin-left: 0;
}
.menu-toggle {
display: block;
}
.grid-2, .grid-3 {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
}
/* Overlay for mobile */
.sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 99;
}
.sidebar-overlay.active {
display: block;
}
/* Loading */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
color: var(--text-muted);
}
.spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 0.5rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Empty state */
.empty-state {
text-align: center;
padding: 3rem;
color: var(--text-muted);
}
.empty-state svg {
width: 48px;
height: 48px;
margin-bottom: 1rem;
opacity: 0.5;
}
/* Badge */
.badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.5rem;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 500;
}
.badge-success { background: rgba(16, 185, 129, 0.2); color: var(--success); }
.badge-warning { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
.badge-danger { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
.badge-info { background: rgba(59, 130, 246, 0.2); color: var(--info); }
/* Analytics Charts */
.chart-bars {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.chart-bar-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
.chart-bar-label {
width: 80px;
font-size: 0.8rem;
color: var(--text-secondary);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.chart-bar-track {
flex: 1;
height: 24px;
background: var(--bg-card);
border-radius: 4px;
overflow: hidden;
}
.chart-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent), #a855f7);
border-radius: 4px;
transition: width 0.5s ease;
min-width: 2px;
}
.chart-bar-value {
width: 40px;
text-align: right;
font-size: 0.8rem;
font-weight: 600;
color: var(--text-primary);
}
/* Mod Breakdown */
.mod-stat-row {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0;
}
.mod-label {
width: 80px;
font-size: 0.875rem;
color: var(--text-secondary);
}
.mod-bar-container {
flex: 1;
height: 12px;
background: var(--bg-card);
border-radius: 6px;
overflow: hidden;
}
.mod-bar {
height: 100%;
border-radius: 6px;
transition: width 0.5s ease;
}
.mod-bar.warning { background: var(--warning); }
.mod-bar.danger { background: var(--danger); }
.mod-bar.purple { background: var(--accent); }
.mod-bar.info { background: var(--info); }
.mod-count {
width: 40px;
text-align: right;
font-size: 0.875rem;
font-weight: 600;
}
/* Heatmap */
.heatmap-container {
display: flex;
flex-direction: column;
gap: 1rem;
}
.heatmap-row {
display: flex;
gap: 0.5rem;
}
.heatmap-labels {
display: flex;
flex-direction: column;
gap: 2px;
font-size: 0.7rem;
color: var(--text-muted);
min-width: 30px;
}
.heatmap-grid {
display: flex;
gap: 2px;
flex: 1;
}
.heatmap-cell {
flex: 1;
aspect-ratio: 1;
min-width: 20px;
max-width: 40px;
background: var(--accent);
border-radius: 3px;
opacity: 0.1;
transition: opacity 0.3s;
}
.heatmap-legend {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
font-size: 0.75rem;
color: var(--text-muted);
}
.legend-boxes {
display: flex;
gap: 2px;
}
.legend-box {
width: 14px;
height: 14px;
background: var(--accent);
border-radius: 2px;
}
/* Weekly Chart */
.weekly-chart {
display: flex;
justify-content: space-around;
align-items: flex-end;
height: 150px;
padding: 1rem 0;
}
.week-bar-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
flex: 1;
}
.week-bar {
width: 40px;
max-width: 60px;
background: linear-gradient(180deg, var(--accent), #a855f7);
border-radius: 4px 4px 0 0;
transition: height 0.5s ease;
min-height: 4px;
}
.week-label {
font-size: 0.75rem;
color: var(--text-muted);
}
/* Leaderboard */
.leaderboard-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 0;
border-bottom: 1px solid var(--border);
}
.leaderboard-item:last-child { border-bottom: none; }
.leaderboard-rank {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-weight: 700;
font-size: 0.875rem;
}
.leaderboard-rank.gold { background: rgba(255, 193, 7, 0.2); color: #ffc107; }
.leaderboard-rank.silver { background: rgba(192, 192, 192, 0.2); color: #c0c0c0; }
.leaderboard-rank.bronze { background: rgba(205, 127, 50, 0.2); color: #cd7f32; }
.leaderboard-rank.default { background: var(--bg-card); color: var(--text-muted); }
.leaderboard-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--bg-tertiary);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
overflow: hidden;
}
.leaderboard-avatar img { width: 100%; height: 100%; object-fit: cover; }
.leaderboard-info { flex: 1; }
.leaderboard-name { font-weight: 500; }
.leaderboard-level {
font-size: 0.8rem;
color: var(--text-muted);
}
.leaderboard-xp {
text-align: right;
font-weight: 600;
color: var(--accent);
}
/* Tooltip */
[data-tooltip] {
position: relative;
}
[data-tooltip]:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: var(--bg-tertiary);
color: var(--text-primary);
padding: 0.5rem 0.75rem;
border-radius: 6px;
font-size: 0.75rem;
white-space: nowrap;
margin-bottom: 0.5rem;
z-index: 100;
}
/* Search input */
.search-input {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.5rem 1rem;
color: var(--text-primary);
font-size: 0.875rem;
width: 100%;
}
.search-input:focus {
outline: none;
border-color: var(--accent);
}
.search-input::placeholder {
color: var(--text-muted);
}
/* Tab buttons */
.tab-buttons {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.tab-btn {
padding: 0.5rem 1rem;
border-radius: 8px;
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-secondary);
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s;
}
.tab-btn:hover {
background: var(--bg-tertiary);
}
.tab-btn.active {
background: var(--accent);
color: white;
border-color: var(--accent);
}
/* Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--bg-primary);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
</style>
</head>
<body>
<!-- Sidebar Overlay (mobile) -->
<div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleSidebar()"></div>
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="logo">A</div>
<div>
<div class="brand-name">AeThex Bot</div>
<div class="brand-tag" id="botTag">Loading...</div>
</div>
</div>
<nav class="nav-section">
<div class="nav-label">Overview</div>
<a class="nav-item active" data-section="overview">
<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="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/></svg>
Dashboard
</a>
<a class="nav-item" data-section="activity">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
Activity Feed
<span class="nav-badge" id="activityBadge" style="display: none;">0</span>
</a>
<div class="nav-label">Analytics</div>
<a class="nav-item" data-section="analytics">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
Statistics
</a>
<a class="nav-item" data-section="leaderboard">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
Leaderboards
</a>
<div class="nav-label">Management</div>
<a class="nav-item" data-section="servers">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><circle cx="6" cy="6" r="1"/><circle cx="6" cy="18" r="1"/></svg>
Servers
</a>
<a class="nav-item" data-section="users">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
User Lookup
</a>
<a class="nav-item" data-section="moderation">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
Moderation
</a>
<a class="nav-item" data-section="tickets">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
Tickets
<span class="nav-badge" id="ticketBadge">0</span>
</a>
<div class="nav-label">Configuration</div>
<a class="nav-item" data-section="config">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
Configuration
</a>
<a class="nav-item" data-section="commands">
<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
</a>
<div class="nav-label">System</div>
<a class="nav-item" data-section="system">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
System Status
</a>
<a class="nav-item" data-section="sentinel">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>
Sentinel Security
</a>
</nav>
<div class="sidebar-footer">
<div style="font-size: 0.75rem; color: var(--text-muted);">
Last updated: <span id="lastUpdate">Never</span>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<header class="topbar">
<div class="topbar-left">
<button class="menu-toggle" onclick="toggleSidebar()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<h1 class="page-title" id="pageTitle">Dashboard</h1>
</div>
<div class="topbar-right">
<div class="status-indicator">
<span class="status-dot" id="statusDot"></span>
<span id="statusText">Connecting...</span>
</div>
<button class="theme-toggle" onclick="toggleTheme()" data-tooltip="Toggle theme">
<svg id="themeIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</button>
<button class="btn btn-primary btn-sm" onclick="refreshData()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
Refresh
</button>
</div>
</header>
<div class="content">
<!-- Overview Section -->
<section class="section active" id="section-overview">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon purple">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><circle cx="6" cy="6" r="1"/><circle cx="6" cy="18" r="1"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">Servers</div>
<div class="stat-value" id="serverCount">-</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon green">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">Total Members</div>
<div class="stat-value" id="memberCount">-</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon blue">
<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>
</div>
<div class="stat-info">
<div class="stat-label">Commands</div>
<div class="stat-value" id="commandCount">-</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon orange">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">Uptime</div>
<div class="stat-value" id="uptime">-</div>
</div>
</div>
</div>
<div class="grid-2">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><circle cx="6" cy="6" r="1"/><circle cx="6" cy="18" r="1"/></svg>
Connected Servers
</h3>
</div>
<div class="card-body" id="serverList">
<div class="loading"><div class="spinner"></div>Loading servers...</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
Sentinel Status
</h3>
</div>
<div class="card-body">
<div class="stats-grid" style="margin-bottom: 0;">
<div style="text-align: center; padding: 1rem;">
<div style="font-size: 2rem; font-weight: 700; color: var(--warning);" id="heatMapSize">0</div>
<div style="font-size: 0.875rem; color: var(--text-muted);">Heat Events</div>
</div>
<div style="text-align: center; padding: 1rem;">
<div style="font-size: 2rem; font-weight: 700; color: var(--info);" id="activeTickets">0</div>
<div style="font-size: 0.875rem; color: var(--text-muted);">Active Tickets</div>
</div>
<div style="text-align: center; padding: 1rem;">
<div style="font-size: 2rem; font-weight: 700; color: var(--success);" id="federationCount">0</div>
<div style="font-size: 0.875rem; color: var(--text-muted);">Federation Links</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Activity Feed Section -->
<section class="section" id="section-activity">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
Live Activity Feed
</h3>
<div class="card-actions">
<button class="btn btn-secondary btn-sm" onclick="clearActivity()">Clear</button>
</div>
</div>
<div class="card-body" id="activityFeed">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
<p>No recent activity</p>
</div>
</div>
</div>
</section>
<!-- Analytics Section -->
<section class="section" id="section-analytics">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon purple">
<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>
</div>
<div class="stat-info">
<div class="stat-label">Commands Today</div>
<div class="stat-value" id="commandsToday">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon green">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">XP Distributed</div>
<div class="stat-value" id="xpToday">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon blue">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">New Members</div>
<div class="stat-value" id="newMembers">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon red">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">Mod Actions</div>
<div class="stat-value" id="modActions">0</div>
</div>
</div>
</div>
<div class="grid-2">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
Command Usage
</h3>
</div>
<div class="card-body">
<div id="commandUsageChart" style="min-height: 200px;">
<div class="chart-bars" id="commandBars"></div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
Moderation Breakdown
</h3>
</div>
<div class="card-body">
<div id="modBreakdown">
<div class="mod-stat-row">
<span class="mod-label">Warnings</span>
<div class="mod-bar-container">
<div class="mod-bar warning" id="warnBar" style="width: 0%"></div>
</div>
<span class="mod-count" id="warnCount">0</span>
</div>
<div class="mod-stat-row">
<span class="mod-label">Kicks</span>
<div class="mod-bar-container">
<div class="mod-bar purple" id="kickBar" style="width: 0%"></div>
</div>
<span class="mod-count" id="kickCountMod">0</span>
</div>
<div class="mod-stat-row">
<span class="mod-label">Bans</span>
<div class="mod-bar-container">
<div class="mod-bar danger" id="banBar" style="width: 0%"></div>
</div>
<span class="mod-count" id="banCountMod">0</span>
</div>
<div class="mod-stat-row">
<span class="mod-label">Timeouts</span>
<div class="mod-bar-container">
<div class="mod-bar info" id="timeoutBar" style="width: 0%"></div>
</div>
<span class="mod-count" id="timeoutCountMod">0</span>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
Activity Heatmap
</h3>
<span class="badge badge-info">Last 24 hours</span>
</div>
<div class="card-body">
<div class="heatmap-container">
<div class="heatmap-row">
<div class="heatmap-labels" id="heatmapLabels"></div>
<div class="heatmap-grid" id="heatmapGrid"></div>
</div>
<div class="heatmap-legend">
<span>Less</span>
<div class="legend-boxes">
<div class="legend-box" style="opacity: 0.2"></div>
<div class="legend-box" style="opacity: 0.4"></div>
<div class="legend-box" style="opacity: 0.6"></div>
<div class="legend-box" style="opacity: 0.8"></div>
<div class="legend-box" style="opacity: 1"></div>
</div>
<span>More</span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
Weekly Activity
</h3>
</div>
<div class="card-body">
<div class="weekly-chart" id="weeklyChart">
<div class="week-bar-wrapper">
<div class="week-bar" id="bar-sun" style="height: 10%"></div>
<span class="week-label">Sun</span>
</div>
<div class="week-bar-wrapper">
<div class="week-bar" id="bar-mon" style="height: 10%"></div>
<span class="week-label">Mon</span>
</div>
<div class="week-bar-wrapper">
<div class="week-bar" id="bar-tue" style="height: 10%"></div>
<span class="week-label">Tue</span>
</div>
<div class="week-bar-wrapper">
<div class="week-bar" id="bar-wed" style="height: 10%"></div>
<span class="week-label">Wed</span>
</div>
<div class="week-bar-wrapper">
<div class="week-bar" id="bar-thu" style="height: 10%"></div>
<span class="week-label">Thu</span>
</div>
<div class="week-bar-wrapper">
<div class="week-bar" id="bar-fri" style="height: 10%"></div>
<span class="week-label">Fri</span>
</div>
<div class="week-bar-wrapper">
<div class="week-bar" id="bar-sat" style="height: 10%"></div>
<span class="week-label">Sat</span>
</div>
</div>
</div>
</div>
</section>
<!-- Leaderboard Section -->
<section class="section" id="section-leaderboard">
<div class="tab-buttons">
<button class="tab-btn active" data-tab="xp">XP Leaders</button>
<button class="tab-btn" data-tab="chatters">Top Chatters</button>
<button class="tab-btn" data-tab="mods">Top Moderators</button>
</div>
<div class="card">
<div class="card-body" id="leaderboardContent">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
<p>Leaderboard data coming soon</p>
</div>
</div>
</div>
</section>
<!-- Servers Section -->
<section class="section" id="section-servers">
<div class="card">
<div class="card-header">
<h3 class="card-title">All Connected Servers</h3>
</div>
<div class="card-body">
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Server</th>
<th>ID</th>
<th>Members</th>
<th>Status</th>
</tr>
</thead>
<tbody id="serversTable">
<tr><td colspan="4" class="loading"><div class="spinner"></div>Loading...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- Users Section -->
<section class="section" id="section-users">
<div class="card">
<div class="card-header">
<h3 class="card-title">User Lookup</h3>
</div>
<div class="card-body">
<div style="max-width: 400px; margin-bottom: 1.5rem;">
<input type="text" class="search-input" placeholder="Enter User ID or Username..." id="userSearch">
</div>
<div id="userResult">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<p>Search for a user to view their profile</p>
</div>
</div>
</div>
</div>
</section>
<!-- Moderation Section -->
<section class="section" id="section-moderation">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon orange">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">Warnings</div>
<div class="stat-value" id="warningCount">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon red">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">Bans</div>
<div class="stat-value" id="banCount">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon blue">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">Timeouts</div>
<div class="stat-value" id="timeoutCount">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon purple">
<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>
</div>
<div class="stat-info">
<div class="stat-label">Kicks</div>
<div class="stat-value" id="kickCount">0</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Recent Mod Actions</h3>
</div>
<div class="card-body" id="modLogContent">
<div class="empty-state">
<p>No recent moderation actions</p>
</div>
</div>
</div>
</section>
<!-- Tickets Section -->
<section class="section" id="section-tickets">
<div class="card">
<div class="card-header">
<h3 class="card-title">Active Tickets</h3>
<span class="badge badge-info" id="ticketCountBadge">0 open</span>
</div>
<div class="card-body" id="ticketList">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
<p>No active tickets</p>
</div>
</div>
</div>
</section>
<!-- Configuration Section -->
<section class="section" id="section-config">
<div class="card">
<div class="card-header">
<h3 class="card-title">Server Configuration</h3>
</div>
<div class="card-body">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
<p>Configuration options coming soon</p>
</div>
</div>
</div>
</section>
<!-- Commands Section -->
<section class="section" id="section-commands">
<div class="card">
<div class="card-header">
<h3 class="card-title">Available Commands</h3>
<span class="badge badge-info" id="totalCommandsBadge">0 commands</span>
</div>
<div class="card-body">
<div class="commands-grid" id="commandsList"></div>
</div>
</div>
</section>
<!-- System Section -->
<section class="section" id="section-system">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon green">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">Uptime</div>
<div class="stat-value" id="systemUptime">-</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon blue">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">Memory Usage</div>
<div class="stat-value" id="memoryUsage">-</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon purple">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">Ping</div>
<div class="stat-value" id="botPing">-</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon orange">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">Node.js</div>
<div class="stat-value" id="nodeVersion">-</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">System Information</h3>
</div>
<div class="card-body">
<table>
<tr><td style="color: var(--text-muted); width: 200px;">Bot Tag</td><td id="sysInfoBotTag">-</td></tr>
<tr><td style="color: var(--text-muted);">Bot ID</td><td id="sysInfoBotId">-</td></tr>
<tr><td style="color: var(--text-muted);">Supabase</td><td id="sysInfoSupabase">-</td></tr>
<tr><td style="color: var(--text-muted);">Feed Bridge</td><td id="sysInfoFeedBridge">-</td></tr>
<tr><td style="color: var(--text-muted);">Started At</td><td id="sysInfoStarted">-</td></tr>
</table>
</div>
</div>
</section>
<!-- Sentinel Section -->
<section class="section" id="section-sentinel">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon red">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">Heat Map Entries</div>
<div class="stat-value" id="sentinelHeatMap">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon orange">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">Threat Level</div>
<div class="stat-value" id="threatLevel">Low</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon green">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">Federation Links</div>
<div class="stat-value" id="sentinelFederation">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon blue">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="17" y1="11" x2="23" y2="11"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">Whitelisted Users</div>
<div class="stat-value" id="whitelistedCount">0</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Threat Monitor</h3>
</div>
<div class="card-body">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
<p>No active threats detected</p>
</div>
</div>
</div>
</section>
</div>
</main>
<script>
// State
let currentSection = 'overview';
let refreshInterval;
const startTime = Date.now();
// Theme toggle
function toggleTheme() {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
}
function updateThemeIcon(theme) {
const icon = document.getElementById('themeIcon');
if (theme === 'light') {
icon.innerHTML = '<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>';
} else {
icon.innerHTML = '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>';
}
}
// Sidebar toggle
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('open');
document.getElementById('sidebarOverlay').classList.toggle('active');
}
// Navigation
document.querySelectorAll('.nav-item[data-section]').forEach(item => {
item.addEventListener('click', () => {
const section = item.getAttribute('data-section');
navigateTo(section);
});
});
function navigateTo(section) {
// Update nav
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.querySelector(`.nav-item[data-section="${section}"]`).classList.add('active');
// Update content
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
document.getElementById(`section-${section}`).classList.add('active');
// Update title
const titles = {
overview: 'Dashboard',
activity: 'Activity Feed',
analytics: 'Statistics',
leaderboard: 'Leaderboards',
servers: 'Servers',
users: 'User Lookup',
moderation: 'Moderation',
tickets: 'Tickets',
config: 'Configuration',
commands: 'Commands',
system: 'System Status',
sentinel: 'Sentinel Security'
};
document.getElementById('pageTitle').textContent = titles[section] || 'Dashboard';
// Close mobile sidebar
document.getElementById('sidebar').classList.remove('open');
document.getElementById('sidebarOverlay').classList.remove('active');
currentSection = section;
}
// Tab buttons
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Format uptime
function formatUptime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) return `${days}d ${hours}h`;
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}
// Format number
function formatNumber(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toString();
}
// Format relative time
function formatTimeAgo(timestamp) {
const seconds = Math.floor((Date.now() - new Date(timestamp)) / 1000);
if (seconds < 60) return 'Just now';
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
return `${Math.floor(seconds / 86400)}d ago`;
}
// Activity type icons and colors
const activityConfig = {
command: { icon: '<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>', color: 'purple' },
member_join: { icon: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/>', color: 'green' },
member_leave: { icon: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="17" y1="11" x2="23" y2="11"/>', color: 'orange' },
mod_action: { icon: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>', color: 'red' },
ticket: { icon: '<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/>', color: 'blue' },
xp: { icon: '<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"/>', color: 'orange' },
alert: { icon: '<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>', color: 'red' },
};
// Render activity event
function renderActivityEvent(event) {
const config = activityConfig[event.type] || activityConfig.command;
let text = '';
switch (event.type) {
case 'command':
text = `<strong>${event.data.user}</strong> used <code>/${event.data.command}</code> in ${event.data.guild}`;
break;
case 'member_join':
text = `<strong>${event.data.user}</strong> joined <strong>${event.data.guild}</strong>`;
break;
case 'member_leave':
text = `<strong>${event.data.user}</strong> left <strong>${event.data.guild}</strong>`;
break;
case 'mod_action':
text = `<strong>${event.data.moderator}</strong> ${event.data.action} <strong>${event.data.target}</strong>`;
break;
case 'ticket':
text = `Ticket ${event.data.action}: <strong>${event.data.reason || 'Support request'}</strong>`;
break;
case 'xp':
text = `<strong>${event.data.user}</strong> earned ${event.data.amount} XP`;
break;
case 'alert':
text = `<span style="color: var(--danger);">${event.data.message}</span>`;
break;
default:
text = JSON.stringify(event.data);
}
return `
<div class="activity-item">
<div class="activity-icon stat-icon ${config.color}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">${config.icon}</svg>
</div>
<div class="activity-content">
<div class="activity-text">${text}</div>
<div class="activity-time">${formatTimeAgo(event.timestamp)}</div>
</div>
</div>
`;
}
// Fetch activity feed
async function fetchActivityFeed() {
try {
const response = await fetch('/activity?limit=20');
const data = await response.json();
const feedContainer = document.getElementById('activityFeed');
if (data.events && data.events.length > 0) {
feedContainer.innerHTML = data.events.map(renderActivityEvent).join('');
const badge = document.getElementById('activityBadge');
badge.textContent = data.events.length;
badge.style.display = 'inline';
} else {
feedContainer.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
<p>No recent activity</p>
</div>
`;
}
} catch (error) {
console.error('Failed to fetch activity:', error);
}
}
// Fetch tickets
async function fetchTickets() {
try {
const response = await fetch('/tickets');
const data = await response.json();
const ticketList = document.getElementById('ticketList');
const ticketBadge = document.getElementById('ticketBadge');
const ticketCountBadge = document.getElementById('ticketCountBadge');
ticketBadge.textContent = data.count || 0;
ticketCountBadge.textContent = `${data.count || 0} open`;
if (data.tickets && data.tickets.length > 0) {
ticketList.innerHTML = data.tickets.map(t => `
<div class="activity-item">
<div class="activity-icon stat-icon blue">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
</div>
<div class="activity-content">
<div class="activity-text"><strong>${t.reason || 'Support Request'}</strong></div>
<div class="activity-time">Open for ${t.age} minutes</div>
</div>
<span class="badge badge-info">Open</span>
</div>
`).join('');
} else {
ticketList.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
<p>No active tickets</p>
</div>
`;
}
} catch (error) {
console.error('Failed to fetch tickets:', error);
}
}
// Fetch threat alerts
async function fetchThreats() {
try {
const response = await fetch('/threats');
const data = await response.json();
document.getElementById('sentinelHeatMap').textContent = data.heatMapSize || 0;
document.getElementById('heatMapSize').textContent = data.heatMapSize || 0;
document.getElementById('threatLevel').textContent = data.threatLevel || 'Low';
const threatLevelEl = document.getElementById('threatLevel');
threatLevelEl.style.color = data.threatLevel === 'Critical' ? 'var(--danger)' :
data.threatLevel === 'High' ? 'var(--warning)' :
data.threatLevel === 'Medium' ? 'var(--info)' : 'var(--success)';
const threatMonitor = document.querySelector('#section-sentinel .card-body:last-child');
if (data.alerts && data.alerts.length > 0) {
threatMonitor.innerHTML = data.alerts.slice(0, 10).map(a => `
<div class="activity-item">
<div class="activity-icon stat-icon ${a.level === 'critical' ? 'red' : a.level === 'high' ? 'orange' : 'blue'}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
</div>
<div class="activity-content">
<div class="activity-text">${a.message}</div>
<div class="activity-time">${formatTimeAgo(a.timestamp)}</div>
</div>
<span class="badge ${a.resolved ? 'badge-success' : 'badge-warning'}">${a.resolved ? 'Resolved' : 'Active'}</span>
</div>
`).join('');
}
} catch (error) {
console.error('Failed to fetch threats:', error);
}
}
// Fetch system info
async function fetchSystemInfo() {
try {
const response = await fetch('/system-info');
const data = await response.json();
document.getElementById('memoryUsage').textContent = `${data.memory.heapUsed}MB`;
document.getElementById('botPing').textContent = `${data.ping}ms`;
document.getElementById('nodeVersion').textContent = data.nodeVersion;
document.getElementById('whitelistedCount').textContent = data.whitelistedUsers || 0;
} catch (error) {
console.error('Failed to fetch system info:', error);
}
}
// Fetch analytics
async function fetchAnalytics() {
try {
const response = await fetch('/analytics');
const data = await response.json();
document.getElementById('commandsToday').textContent = data.commandsToday || 0;
document.getElementById('xpToday').textContent = formatNumber(data.xpDistributed || 0);
document.getElementById('newMembers').textContent = data.newMembers || 0;
document.getElementById('modActions').textContent = data.modActionsTotal || 0;
const mods = data.modActions || {};
const maxMod = Math.max(mods.warnings || 0, mods.kicks || 0, mods.bans || 0, mods.timeouts || 0, 1);
document.getElementById('warnCount').textContent = mods.warnings || 0;
document.getElementById('kickCountMod').textContent = mods.kicks || 0;
document.getElementById('banCountMod').textContent = mods.bans || 0;
document.getElementById('timeoutCountMod').textContent = mods.timeouts || 0;
document.getElementById('warnBar').style.width = `${((mods.warnings || 0) / maxMod) * 100}%`;
document.getElementById('kickBar').style.width = `${((mods.kicks || 0) / maxMod) * 100}%`;
document.getElementById('banBar').style.width = `${((mods.bans || 0) / maxMod) * 100}%`;
document.getElementById('timeoutBar').style.width = `${((mods.timeouts || 0) / maxMod) * 100}%`;
const commandBars = document.getElementById('commandBars');
if (data.commandUsage && data.commandUsage.length > 0) {
const maxCmd = data.commandUsage[0].count || 1;
commandBars.innerHTML = data.commandUsage.slice(0, 8).map(cmd => `
<div class="chart-bar-row">
<span class="chart-bar-label">/${cmd.name}</span>
<div class="chart-bar-track">
<div class="chart-bar-fill" style="width: ${(cmd.count / maxCmd) * 100}%"></div>
</div>
<span class="chart-bar-value">${cmd.count}</span>
</div>
`).join('');
} else {
commandBars.innerHTML = '<div class="empty-state" style="padding: 1rem;"><p>No command usage yet</p></div>';
}
const heatmapGrid = document.getElementById('heatmapGrid');
if (data.hourlyActivity && data.hourlyActivity.length === 24) {
const maxHour = Math.max(...data.hourlyActivity, 1);
heatmapGrid.innerHTML = data.hourlyActivity.map((count, hour) => {
const opacity = 0.1 + (count / maxHour) * 0.9;
return `<div class="heatmap-cell" style="opacity: ${opacity}" title="${hour}:00 - ${count} events"></div>`;
}).join('');
}
if (data.dailyActivity && data.dailyActivity.length === 7) {
const maxDay = Math.max(...data.dailyActivity, 1);
const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
days.forEach((day, i) => {
const height = 10 + (data.dailyActivity[i] / maxDay) * 90;
const bar = document.getElementById(`bar-${day}`);
if (bar) bar.style.height = `${height}%`;
});
}
} catch (error) {
console.error('Failed to fetch analytics:', error);
}
}
// Fetch leaderboard
async function fetchLeaderboard() {
try {
const response = await fetch('/leaderboard');
const data = await response.json();
const leaderboardContent = document.getElementById('leaderboardContent');
if (data.success && data.xpLeaders && data.xpLeaders.length > 0) {
leaderboardContent.innerHTML = data.xpLeaders.map(user => {
const rankClass = user.rank === 1 ? 'gold' : user.rank === 2 ? 'silver' : user.rank === 3 ? 'bronze' : 'default';
const initial = (user.username || 'U').charAt(0).toUpperCase();
return `
<div class="leaderboard-item">
<div class="leaderboard-rank ${rankClass}">${user.rank}</div>
<div class="leaderboard-avatar">${user.avatarUrl ? `<img src="${user.avatarUrl}" alt="">` : initial}</div>
<div class="leaderboard-info">
<div class="leaderboard-name">${user.username || 'Unknown'}</div>
<div class="leaderboard-level">Level ${user.level}</div>
</div>
<div class="leaderboard-xp">${formatNumber(user.xp)} XP</div>
</div>
`;
}).join('');
} else {
leaderboardContent.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
<p>No leaderboard data available</p>
</div>
`;
}
} catch (error) {
console.error('Failed to fetch leaderboard:', error);
}
}
// Fetch and update data
async function refreshData() {
try {
const [health, stats] = await Promise.all([
fetch('/health').then(r => r.json()),
fetch('/stats').then(r => r.json())
]);
// Status
const isOnline = health.status === 'online';
document.getElementById('statusDot').className = `status-dot ${isOnline ? '' : 'offline'}`;
document.getElementById('statusText').textContent = isOnline ? 'Online' : 'Offline';
// Overview stats
document.getElementById('serverCount').textContent = health.guilds;
document.getElementById('memberCount').textContent = formatNumber(stats.totalMembers);
document.getElementById('commandCount').textContent = health.commands;
document.getElementById('uptime').textContent = formatUptime(health.uptime);
// Sentinel stats
document.getElementById('heatMapSize').textContent = health.heatMapSize || 0;
document.getElementById('activeTickets').textContent = stats.activeTickets || 0;
document.getElementById('federationCount').textContent = stats.federationLinks || 0;
document.getElementById('ticketBadge').textContent = stats.activeTickets || 0;
document.getElementById('sentinelFederation').textContent = stats.federationLinks || 0;
// Server list
const serverList = document.getElementById('serverList');
if (stats.guilds && stats.guilds.length > 0) {
serverList.innerHTML = stats.guilds.map(g => `
<div class="server-item">
<div class="server-icon">${g.name.charAt(0).toUpperCase()}</div>
<div class="server-info">
<div class="server-name">${g.name}</div>
<div class="server-members">${formatNumber(g.memberCount)} members</div>
</div>
<span class="badge badge-success">Active</span>
</div>
`).join('');
}
// Servers table
const serversTable = document.getElementById('serversTable');
if (stats.guilds && stats.guilds.length > 0) {
serversTable.innerHTML = stats.guilds.map(g => `
<tr>
<td><strong>${g.name}</strong></td>
<td style="font-family: monospace; font-size: 0.8rem; color: var(--text-muted);">${g.id}</td>
<td>${formatNumber(g.memberCount)}</td>
<td><span class="badge badge-success">Online</span></td>
</tr>
`).join('');
}
// Commands list
const commands = ['admin', 'announce', 'auditlog', 'avatar', 'badges', 'ban', 'config',
'daily', 'federation', 'foundation', 'help', 'kick', 'leaderboard',
'modlog', 'poll', 'post', 'profile', 'rank', 'refresh-roles',
'serverinfo', 'set-realm', 'stats', 'status', 'studio', 'ticket',
'timeout', 'unlink', 'userinfo', 'verify', 'verify-role', 'warn'];
document.getElementById('commandsList').innerHTML = commands.map(c =>
`<div class="command-item">${c}</div>`
).join('');
document.getElementById('totalCommandsBadge').textContent = `${commands.length} commands`;
// System info
document.getElementById('systemUptime').textContent = formatUptime(health.uptime);
document.getElementById('sysInfoSupabase').innerHTML = health.supabaseConnected
? '<span class="badge badge-success">Connected</span>'
: '<span class="badge badge-warning">Not Configured</span>';
// Sentinel stats (in section)
document.getElementById('sentinelHeatMap').textContent = health.heatMapSize || 0;
// Last update
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
// Fetch additional real-time data
await Promise.all([
fetchActivityFeed(),
fetchTickets(),
fetchThreats(),
fetchSystemInfo(),
fetchAnalytics(),
fetchLeaderboard()
]);
} catch (error) {
console.error('Failed to fetch data:', error);
document.getElementById('statusDot').className = 'status-dot offline';
document.getElementById('statusText').textContent = 'Error';
}
}
// Try to fetch extended bot status
async function fetchExtendedStatus() {
try {
const token = localStorage.getItem('adminToken') || '';
const response = await fetch('/bot-status', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
const data = await response.json();
document.getElementById('botTag').textContent = data.bot?.tag || 'Unknown';
document.getElementById('sysInfoBotTag').textContent = data.bot?.tag || '-';
document.getElementById('sysInfoBotId').textContent = data.bot?.id || '-';
document.getElementById('sysInfoFeedBridge').innerHTML = data.feedBridge?.enabled
? '<span class="badge badge-success">Enabled</span>'
: '<span class="badge badge-warning">Disabled</span>';
}
} catch (e) {
console.log('Extended status not available');
}
}
function clearActivity() {
document.getElementById('activityFeed').innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
<p>No recent activity</p>
</div>
`;
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
// Load theme
const savedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
// Initial fetch
refreshData();
fetchExtendedStatus();
// Auto-refresh every 30s
refreshInterval = setInterval(refreshData, 30000);
});
</script>
</body>
</html>