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
2105 lines
75 KiB
HTML
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>
|