AeThex-Bot-Master/aethex-bot/public/dashboard.html
sirpiglr cd6f84fd4d Add detailed analytics and charts for command usage trends
Integrate new '/command-analytics' endpoint to display daily trends, hourly activity, and top users in the dashboard, alongside updating the existing '/analytics' data fetching to support concurrent requests.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 0275c4be-7268-43b8-a8d0-8f97a0077139
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/4ZEVdt6
Replit-Helium-Checkpoint-Created: true
2025-12-08 06:40:51 +00:00

3967 lines
158 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);
}
/* Config tabs */
.config-tab {
display: none;
}
.config-tab.active {
display: block;
}
/* Form elements */
.form-group {
margin-bottom: 1.25rem;
}
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.form-hint {
display: block;
font-size: 0.75rem;
color: var(--text-muted);
margin-top: 0.25rem;
}
.form-input, .form-select, .form-textarea {
width: 100%;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.625rem 0.875rem;
color: var(--text-primary);
font-size: 0.875rem;
transition: border-color 0.2s;
}
.form-input:focus, .form-select:focus, .form-textarea:focus {
outline: none;
border-color: var(--accent);
}
.form-input::placeholder, .form-textarea::placeholder {
color: var(--text-muted);
}
.form-textarea {
resize: vertical;
min-height: 100px;
}
.form-select {
cursor: pointer;
}
/* Checkbox group */
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--text-secondary);
cursor: pointer;
}
.checkbox-item input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--accent);
}
/* Role item */
.role-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem;
background: var(--bg-card);
border-radius: 8px;
margin-bottom: 0.5rem;
}
.role-item:last-child {
margin-bottom: 0;
}
.role-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.role-color {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--accent);
}
.role-name {
font-weight: 500;
}
.role-meta {
font-size: 0.8rem;
color: var(--text-muted);
}
/* Whitelist item */
.whitelist-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.625rem 0;
border-bottom: 1px solid var(--border);
}
.whitelist-item:last-child {
border-bottom: none;
}
.whitelist-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.whitelist-icon {
width: 32px;
height: 32px;
border-radius: 8px;
background: var(--bg-tertiary);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.875rem;
}
.whitelist-name {
font-weight: 500;
}
.whitelist-id {
font-size: 0.75rem;
color: var(--text-muted);
font-family: monospace;
}
/* Announcement item */
.announce-item {
padding: 1rem;
background: var(--bg-card);
border-radius: 8px;
margin-bottom: 0.75rem;
}
.announce-item:last-child {
margin-bottom: 0;
}
.announce-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.announce-title {
font-weight: 600;
}
.announce-time {
font-size: 0.75rem;
color: var(--text-muted);
}
.announce-message {
font-size: 0.875rem;
color: var(--text-secondary);
}
/* Toast notification */
.toast {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
background: var(--bg-secondary);
border: 1px solid var(--border);
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
display: flex;
align-items: center;
gap: 0.75rem;
z-index: 1000;
animation: slideIn 0.3s ease;
}
.toast.success { border-left: 4px solid var(--success); }
.toast.error { border-left: 4px solid var(--danger); }
.toast.warning { border-left: 4px solid var(--warning); }
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* 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>
<a class="nav-item" data-section="integrations">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
Integrations
</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>
<!-- Detailed Analytics Charts (Database-backed) -->
<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"><path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/></svg>
Daily Command Trends
</h3>
<span class="badge badge-info">Last 7 days</span>
</div>
<div class="card-body">
<div id="dailyLineChart" style="min-height: 120px;">
<div class="empty-state" style="padding: 1rem;">
<p style="color: var(--text-muted);">Loading chart data...</p>
</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"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Hourly Activity (24h)
</h3>
</div>
<div class="card-body">
<div id="hourlyBarChart" style="min-height: 100px;">
<div class="empty-state" style="padding: 1rem;">
<p style="color: var(--text-muted);">Loading chart data...</p>
</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"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><polyline points="17 11 19 13 23 9"/></svg>
Top Command Users
</h3>
<span class="badge badge-info">Last 7 days</span>
</div>
<div class="card-body">
<div id="topUsersChart" style="min-height: 150px;">
<div class="empty-state" style="padding: 1rem;">
<p style="color: var(--text-muted);">No user data available yet</p>
</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">
<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>
User Lookup
</h3>
</div>
<div class="card-body">
<div style="display: flex; gap: 0.75rem; max-width: 500px; margin-bottom: 1.5rem;">
<input type="text" class="search-input" placeholder="Enter User ID or Username..." id="userSearch" onkeypress="if(event.key==='Enter')searchUser()">
<button class="btn btn-primary" onclick="searchUser()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
Search
</button>
</div>
<div id="userSearchLoading" style="display: none;" class="loading"><div class="spinner"></div>Searching users...</div>
<div id="userResult">
<div class="empty-state">
<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>
<p>Search for a user by Discord ID or username</p>
<p style="font-size: 0.8rem; margin-top: 0.5rem;">View profile, XP, warnings, and mod history</p>
</div>
</div>
</div>
</div>
<div id="userProfileCard" class="card" style="display: none;">
<div class="card-header">
<h3 class="card-title">User Profile</h3>
<button class="btn btn-secondary btn-sm" onclick="clearUserSearch()">Clear</button>
</div>
<div class="card-body" id="userProfileContent"></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="tab-buttons" style="margin-bottom: 1.5rem;">
<button class="tab-btn active" onclick="switchConfigTab('settings')">Server Settings</button>
<button class="tab-btn" onclick="switchConfigTab('roles')">Role Management</button>
<button class="tab-btn" onclick="switchConfigTab('whitelist')">Whitelist</button>
<button class="tab-btn" onclick="switchConfigTab('announcements')">Announcements</button>
</div>
<!-- Server Settings Tab -->
<div class="config-tab active" id="config-settings">
<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>
Select Server
</h3>
</div>
<div class="card-body">
<select class="form-select" id="configServerSelect" onchange="loadServerConfig()">
<option value="">Select a server...</option>
</select>
</div>
</div>
<div id="serverConfigPanel" style="display: none;">
<div class="grid-2">
<div class="card">
<div class="card-header">
<h3 class="card-title">Channel Settings</h3>
</div>
<div class="card-body">
<div class="form-group">
<label class="form-label">Welcome Channel</label>
<input type="text" class="form-input" id="configWelcomeChannel" placeholder="Channel ID">
<span class="form-hint">Messages sent when members join</span>
</div>
<div class="form-group">
<label class="form-label">Goodbye Channel</label>
<input type="text" class="form-input" id="configGoodbyeChannel" placeholder="Channel ID">
<span class="form-hint">Messages sent when members leave</span>
</div>
<div class="form-group">
<label class="form-label">Mod Log Channel</label>
<input type="text" class="form-input" id="configModlogChannel" placeholder="Channel ID">
<span class="form-hint">Moderation action logs</span>
</div>
<div class="form-group">
<label class="form-label">Level-Up Channel</label>
<input type="text" class="form-input" id="configLevelupChannel" placeholder="Channel ID">
<span class="form-hint">Level-up announcements</span>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Role Settings</h3>
</div>
<div class="card-body">
<div class="form-group">
<label class="form-label">Auto-Role</label>
<input type="text" class="form-input" id="configAutoRole" placeholder="Role ID">
<span class="form-hint">Role given to new members</span>
</div>
<div class="form-group">
<label class="form-label">Verified Role</label>
<input type="text" class="form-input" id="configVerifiedRole" placeholder="Role ID">
<span class="form-hint">Role given after verification</span>
</div>
</div>
</div>
</div>
<div class="card" style="margin-top: 1rem;">
<div class="card-body" style="display: flex; gap: 1rem; justify-content: flex-end;">
<button class="btn btn-secondary" onclick="loadServerConfig()">
<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>
Reset
</button>
<button class="btn btn-primary" onclick="saveServerConfig()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
Save Configuration
</button>
</div>
</div>
</div>
</div>
<!-- Role Management Tab -->
<div class="config-tab" id="config-roles">
<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"><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>
Level Roles
</h3>
<button class="btn btn-primary btn-sm" onclick="showAddLevelRoleModal()">Add Role</button>
</div>
<div class="card-body" id="levelRolesList">
<div class="empty-state">
<p>No level roles configured</p>
</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>
Federation Roles
</h3>
</div>
<div class="card-body" id="federationRolesList">
<div class="empty-state">
<p>No federation roles linked</p>
</div>
</div>
</div>
</div>
</div>
<!-- Whitelist Tab -->
<div class="config-tab" id="config-whitelist">
<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>
Whitelisted Servers
</h3>
</div>
<div class="card-body" id="whitelistServersList">
<div class="loading"><div class="spinner"></div>Loading...</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="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>
Whitelisted Users (Heat Skip)
</h3>
</div>
<div class="card-body" id="whitelistUsersList">
<div class="loading"><div class="spinner"></div>Loading...</div>
</div>
</div>
</div>
</div>
<!-- Announcements Tab -->
<div class="config-tab" id="config-announcements">
<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="m3 11 18-5v12L3 13v-2z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/></svg>
Send Announcement
</h3>
</div>
<div class="card-body">
<div class="form-group">
<label class="form-label">Title</label>
<input type="text" class="form-input" id="announceTitle" placeholder="Announcement title...">
</div>
<div class="form-group">
<label class="form-label">Message</label>
<textarea class="form-textarea" id="announceMessage" rows="4" placeholder="Announcement content..."></textarea>
</div>
<div class="form-group">
<label class="form-label">Target Servers</label>
<div id="announceTargets" class="checkbox-group">
<label class="checkbox-item">
<input type="checkbox" value="all" checked> All Servers
</label>
</div>
</div>
<div style="display: flex; gap: 1rem; justify-content: flex-end; margin-top: 1rem;">
<button class="btn btn-secondary" onclick="clearAnnouncement()">Clear</button>
<button class="btn btn-primary" onclick="sendAnnouncement()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
Send Announcement
</button>
</div>
</div>
</div>
<div class="card" style="margin-top: 1.5rem;">
<div class="card-header">
<h3 class="card-title">Recent Announcements</h3>
</div>
<div class="card-body" id="recentAnnouncements">
<div class="empty-state">
<p>No recent announcements</p>
</div>
</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>
<!-- Integrations Section -->
<section class="section" id="section-integrations">
<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"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>
AeThex Studio Feed
</h3>
<button class="btn btn-secondary btn-sm" onclick="fetchStudioFeed()">
<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>
<div class="card-body" id="studioFeedContent" style="max-height: 400px; overflow-y: auto;">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg>
<p>Loading Studio feed...</p>
</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="M3 3v18h18"/><path d="M18.7 8l-5.1 5.2-2.8-2.7L7 14.3"/></svg>
Foundation Tracker
</h3>
<button class="btn btn-secondary btn-sm" onclick="fetchFoundationStats()">
<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>
<div class="card-body" id="foundationContent">
<div class="stats-grid" style="grid-template-columns: repeat(2, 1fr); margin-bottom: 1rem;">
<div style="text-align: center; padding: 0.75rem; background: var(--bg-card); border-radius: 8px;">
<div style="font-size: 1.5rem; font-weight: 700; color: var(--success);" id="foundationContributors">-</div>
<div style="font-size: 0.75rem; color: var(--text-muted);">Contributors</div>
</div>
<div style="text-align: center; padding: 0.75rem; background: var(--bg-card); border-radius: 8px;">
<div style="font-size: 1.5rem; font-weight: 700; color: var(--accent);" id="foundationProjects">-</div>
<div style="font-size: 0.75rem; color: var(--text-muted);">Projects</div>
</div>
<div style="text-align: center; padding: 0.75rem; background: var(--bg-card); border-radius: 8px;">
<div style="font-size: 1.5rem; font-weight: 700; color: var(--info);" id="foundationCommits">-</div>
<div style="font-size: 0.75rem; color: var(--text-muted);">Commits</div>
</div>
<div style="text-align: center; padding: 0.75rem; background: var(--bg-card); border-radius: 8px;">
<div style="font-size: 1.5rem; font-weight: 700; color: var(--warning);" id="foundationMembers">-</div>
<div style="font-size: 0.75rem; color: var(--text-muted);">Members</div>
</div>
</div>
<h4 style="margin-bottom: 0.75rem; font-size: 0.9rem;">Recent Activity</h4>
<div id="foundationActivity" style="max-height: 200px; overflow-y: auto;">
<div class="empty-state" style="padding: 1rem;">
<p style="font-size: 0.875rem;">Loading foundation activity...</p>
</div>
</div>
</div>
</div>
</div>
<div class="card" style="margin-top: 1.5rem;">
<div class="card-header">
<h3 class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
Webhook Tester
</h3>
</div>
<div class="card-body">
<div class="grid-2">
<div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.875rem; color: var(--text-secondary);">Webhook URL</label>
<input type="text" id="webhookUrl" class="search-input" placeholder="https://discord.com/api/webhooks/...">
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.875rem; color: var(--text-secondary);">Message Content</label>
<textarea id="webhookMessage" class="search-input" style="height: 80px; resize: vertical;" placeholder="Test message content..."></textarea>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.875rem; color: var(--text-secondary);">Username (optional)</label>
<input type="text" id="webhookUsername" class="search-input" placeholder="AeThex Bot">
</div>
<button class="btn btn-primary" onclick="testWebhook()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
Send Test Message
</button>
</div>
<div>
<h4 style="margin-bottom: 0.75rem;">Webhook Test Log</h4>
<div id="webhookTestLog" style="background: var(--bg-card); border-radius: 8px; padding: 1rem; height: 200px; overflow-y: auto; font-family: monospace; font-size: 0.8rem;">
<div style="color: var(--text-muted);">No tests run yet...</div>
</div>
</div>
</div>
</div>
</div>
<div class="card" style="margin-top: 1.5rem;">
<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="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
Integration Status
</h3>
</div>
<div class="card-body">
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Integration</th>
<th>Status</th>
<th>Last Sync</th>
<th>Details</th>
</tr>
</thead>
<tbody id="integrationStatusTable">
<tr>
<td>Discord API</td>
<td><span class="badge badge-success" id="discordApiStatus">Connected</span></td>
<td id="discordApiSync">-</td>
<td>Gateway connection active</td>
</tr>
<tr>
<td>Supabase</td>
<td><span class="badge" id="supabaseStatus">-</span></td>
<td id="supabaseSync">-</td>
<td id="supabaseDetails">-</td>
</tr>
<tr>
<td>Feed Bridge</td>
<td><span class="badge" id="feedBridgeStatus">-</span></td>
<td id="feedBridgeSync">-</td>
<td id="feedBridgeDetails">-</td>
</tr>
<tr>
<td>AeThex Studio</td>
<td><span class="badge badge-info">Polling</span></td>
<td id="studioSync">-</td>
<td>Community feed sync</td>
</tr>
<tr>
<td>AeThex Foundation</td>
<td><span class="badge badge-info">Polling</span></td>
<td id="foundationSync">-</td>
<td>Contribution tracker</td>
</tr>
</tbody>
</table>
</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 red">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="2" x2="9" y2="4"/><line x1="15" y1="2" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="22"/><line x1="15" y1="20" x2="15" y2="22"/><line x1="20" y1="9" x2="22" y2="9"/><line x1="20" y1="15" x2="22" y2="15"/><line x1="2" y1="9" x2="4" y2="9"/><line x1="2" y1="15" x2="4" y2="15"/></svg>
</div>
<div class="stat-info">
<div class="stat-label">CPU Usage</div>
<div class="stat-value" id="cpuUsage">-</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 class="stat-card">
<div class="stat-icon blue">
<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>
</div>
<div class="stat-info">
<div class="stat-label">Command Queue</div>
<div class="stat-value" id="commandQueueCount">-</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"><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>
System Information
</h3>
</div>
<div class="card-body">
<table>
<tr><td style="color: var(--text-muted); width: 180px;">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);">Platform</td><td id="sysInfoPlatform">-</td></tr>
<tr><td style="color: var(--text-muted);">Started At</td><td id="sysInfoStarted">-</td></tr>
</table>
</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>
Command Queue
</h3>
<div class="card-actions">
<span class="badge badge-success" id="queueCompletedBadge">0 completed</span>
<span class="badge badge-warning" id="queuePendingBadge">0 pending</span>
<span class="badge badge-danger" id="queueFailedBadge">0 failed</span>
</div>
</div>
<div class="card-body" id="commandQueueList" style="max-height: 250px; overflow-y: auto;">
<div class="empty-state"><p>No commands in queue</p></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="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>
Error Logs
</h3>
<div class="card-actions">
<span class="badge badge-danger" id="errorCountBadge">0 errors</span>
<button class="btn btn-sm btn-secondary" onclick="clearErrorLogs()">Clear</button>
</div>
</div>
<div class="card-body" id="errorLogsContent" style="max-height: 300px; overflow-y: auto;">
<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 errors logged</p>
</div>
</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;
document.getElementById('cpuUsage').textContent = `${data.cpu || 0}%`;
document.getElementById('sysInfoPlatform').textContent = data.platform || '-';
const queue = data.commandQueue || { pending: 0, completed: 0, failed: 0, recent: [] };
document.getElementById('commandQueueCount').textContent = queue.pending || 0;
document.getElementById('queueCompletedBadge').textContent = `${queue.completed} completed`;
document.getElementById('queuePendingBadge').textContent = `${queue.pending} pending`;
document.getElementById('queueFailedBadge').textContent = `${queue.failed} failed`;
const queueList = document.getElementById('commandQueueList');
if (queue.recent && queue.recent.length > 0) {
queueList.innerHTML = queue.recent.map(cmd => `
<div class="activity-item">
<div class="activity-icon stat-icon ${cmd.status === 'completed' ? 'green' : cmd.status === 'failed' ? 'red' : 'blue'}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
${cmd.status === 'completed' ? '<polyline points="20 6 9 17 4 12"/>' :
cmd.status === 'failed' ? '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>' :
'<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>'}
</svg>
</div>
<div class="activity-content">
<div class="activity-text">${cmd.command || 'Unknown command'}</div>
<div class="activity-time">
<span class="badge ${cmd.status === 'completed' ? 'badge-success' : cmd.status === 'failed' ? 'badge-danger' : 'badge-warning'}">${cmd.status}</span>
${formatTimeAgo(cmd.timestamp)}
</div>
</div>
</div>
`).join('');
} else {
queueList.innerHTML = '<div class="empty-state"><p>No commands in queue</p></div>';
}
const errorLogs = data.errorLogs || [];
document.getElementById('errorCountBadge').textContent = `${data.errorCount || 0} errors`;
const errorLogsContent = document.getElementById('errorLogsContent');
if (errorLogs.length > 0) {
errorLogsContent.innerHTML = errorLogs.map(err => `
<div class="activity-item">
<div class="activity-icon stat-icon red">
<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"/></svg>
</div>
<div class="activity-content">
<div class="activity-text"><strong>${err.type || 'Error'}</strong>: ${err.message || 'Unknown error'}</div>
<div class="activity-time">
${err.details?.command ? `/${err.details.command} by ${err.details.user || 'Unknown'} ` : ''}
${formatTimeAgo(err.timestamp)}
</div>
${err.details?.error ? `<div style="font-size: 0.75rem; color: var(--danger); margin-top: 0.25rem; font-family: monospace;">${err.details.error}</div>` : ''}
</div>
</div>
`).join('');
} else {
errorLogsContent.innerHTML = `
<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 errors logged</p>
</div>
`;
}
} catch (error) {
console.error('Failed to fetch system info:', error);
}
}
function clearErrorLogs() {
showToast('Error logs are automatically cleared after 24 hours', 'info');
}
// Fetch analytics
async function fetchAnalytics() {
try {
const [analyticsRes, cmdAnalyticsRes] = await Promise.all([
fetch('/analytics'),
fetch('/command-analytics?days=7')
]);
const data = await analyticsRes.json();
const cmdAnalytics = await cmdAnalyticsRes.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}%`;
// Use database command analytics if available
const commandBars = document.getElementById('commandBars');
const commands = cmdAnalytics.commands && cmdAnalytics.commands.length > 0
? cmdAnalytics.commands
: (data.commandUsage || []);
if (commands.length > 0) {
const maxCmd = Math.max(...commands.map(c => parseInt(c.count) || 0), 1);
commandBars.innerHTML = commands.slice(0, 8).map(cmd => {
const name = cmd.command_name || cmd.name || 'unknown';
const count = parseInt(cmd.count) || 0;
const successRate = cmd.success_count ? Math.round((parseInt(cmd.success_count) / count) * 100) : 100;
const avgTime = cmd.avg_time ? Math.round(parseFloat(cmd.avg_time)) : 0;
return `
<div class="chart-bar-row">
<span class="chart-bar-label" title="Avg: ${avgTime}ms, Success: ${successRate}%">/${name}</span>
<div class="chart-bar-track">
<div class="chart-bar-fill" style="width: ${(count / maxCmd) * 100}%"></div>
</div>
<span class="chart-bar-value">${count}</span>
</div>
`;
}).join('');
} else {
commandBars.innerHTML = '<div class="empty-state" style="padding: 1rem;"><p>No command usage yet</p></div>';
}
// Render daily line chart from database
const dailyChartEl = document.getElementById('dailyLineChart');
if (dailyChartEl && cmdAnalytics.daily && cmdAnalytics.daily.length > 0) {
renderDailyLineChart(dailyChartEl, cmdAnalytics.daily);
}
// Render hourly chart from database
const hourlyChartEl = document.getElementById('hourlyBarChart');
if (hourlyChartEl && cmdAnalytics.hourly && cmdAnalytics.hourly.length > 0) {
renderHourlyChart(hourlyChartEl, cmdAnalytics.hourly);
}
// Render top users
const topUsersEl = document.getElementById('topUsersChart');
if (topUsersEl && cmdAnalytics.topUsers && cmdAnalytics.topUsers.length > 0) {
renderTopUsersChart(topUsersEl, cmdAnalytics.topUsers);
}
// Fallback heatmap from in-memory data
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);
}
}
// Render daily line chart (SVG-based)
function renderDailyLineChart(container, data) {
const width = container.clientWidth || 400;
const height = 120;
const padding = 30;
if (data.length === 0) {
container.innerHTML = '<p style="color: var(--text-muted); text-align: center;">No data available</p>';
return;
}
const counts = data.map(d => parseInt(d.count) || 0);
const maxCount = Math.max(...counts, 1);
const minCount = 0;
const xStep = (width - padding * 2) / Math.max(data.length - 1, 1);
const yScale = (height - padding * 2) / (maxCount - minCount || 1);
const points = data.map((d, i) => ({
x: padding + i * xStep,
y: height - padding - (parseInt(d.count) || 0) * yScale,
date: d.date,
count: parseInt(d.count) || 0
}));
const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ');
const areaPath = `${linePath} L ${points[points.length - 1].x} ${height - padding} L ${points[0].x} ${height - padding} Z`;
container.innerHTML = `
<svg width="100%" height="${height}" viewBox="0 0 ${width} ${height}" preserveAspectRatio="xMidYMid meet">
<defs>
<linearGradient id="areaGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:var(--accent);stop-opacity:0.3"/>
<stop offset="100%" style="stop-color:var(--accent);stop-opacity:0"/>
</linearGradient>
</defs>
<path d="${areaPath}" fill="url(#areaGradient)"/>
<path d="${linePath}" fill="none" stroke="var(--accent)" stroke-width="2"/>
${points.map(p => `
<circle cx="${p.x}" cy="${p.y}" r="4" fill="var(--accent)" stroke="var(--bg-secondary)" stroke-width="2">
<title>${p.date}: ${p.count} commands</title>
</circle>
`).join('')}
<text x="${padding}" y="${height - 5}" fill="var(--text-muted)" font-size="10">${data[0]?.date?.split('-').slice(1).join('/') || ''}</text>
<text x="${width - padding}" y="${height - 5}" fill="var(--text-muted)" font-size="10" text-anchor="end">${data[data.length - 1]?.date?.split('-').slice(1).join('/') || ''}</text>
</svg>
`;
}
// Render hourly bar chart
function renderHourlyChart(container, data) {
const hours = Array(24).fill(0);
data.forEach(d => {
const h = parseInt(d.hour);
if (h >= 0 && h < 24) hours[h] = parseInt(d.count) || 0;
});
const maxCount = Math.max(...hours, 1);
container.innerHTML = `
<div style="display: flex; align-items: flex-end; height: 80px; gap: 2px;">
${hours.map((count, h) => {
const heightPct = 10 + (count / maxCount) * 90;
return `
<div style="flex: 1; height: ${heightPct}%; background: linear-gradient(180deg, var(--accent), #a855f7); border-radius: 2px 2px 0 0; min-width: 8px;" title="${h}:00 - ${count} commands"></div>
`;
}).join('')}
</div>
<div style="display: flex; justify-content: space-between; margin-top: 4px; font-size: 0.7rem; color: var(--text-muted);">
<span>0:00</span>
<span>6:00</span>
<span>12:00</span>
<span>18:00</span>
<span>23:00</span>
</div>
`;
}
// Render top users chart
function renderTopUsersChart(container, data) {
if (data.length === 0) {
container.innerHTML = '<p style="color: var(--text-muted);">No user data available</p>';
return;
}
const maxCount = Math.max(...data.map(d => parseInt(d.command_count) || 0), 1);
container.innerHTML = data.slice(0, 5).map((user, i) => {
const count = parseInt(user.command_count) || 0;
const pct = (count / maxCount) * 100;
const rankClass = i === 0 ? 'gold' : i === 1 ? 'silver' : i === 2 ? 'bronze' : 'default';
const tag = user.user_tag || `User ${user.user_id?.slice(-4) || '????'}`;
return `
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<span class="leaderboard-rank ${rankClass}" style="width: 24px; height: 24px; font-size: 0.75rem;">${i + 1}</span>
<span style="width: 100px; font-size: 0.8rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${tag}</span>
<div style="flex: 1; height: 16px; background: var(--bg-card); border-radius: 4px; overflow: hidden;">
<div style="width: ${pct}%; height: 100%; background: linear-gradient(90deg, var(--accent), #a855f7); border-radius: 4px;"></div>
</div>
<span style="width: 40px; text-align: right; font-size: 0.8rem; font-weight: 600;">${count}</span>
</div>
`;
}).join('');
}
// 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>
`;
}
// ============================================
// MANAGEMENT PANEL FUNCTIONS
// ============================================
let cachedServers = [];
let currentServerConfig = null;
function switchConfigTab(tabName) {
document.querySelectorAll('.config-tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('#section-config .tab-btn').forEach(btn => btn.classList.remove('active'));
document.getElementById(`config-${tabName}`).classList.add('active');
event.target.classList.add('active');
if (tabName === 'settings') {
populateServerSelect();
} else if (tabName === 'whitelist') {
fetchWhitelist();
} else if (tabName === 'roles') {
fetchRoles();
} else if (tabName === 'announcements') {
populateAnnounceTargets();
}
}
async function populateServerSelect() {
try {
const response = await fetch('/stats');
const data = await response.json();
cachedServers = data.guilds || [];
const select = document.getElementById('configServerSelect');
select.innerHTML = '<option value="">Select a server...</option>' +
cachedServers.map(g => `<option value="${g.id}">${g.name}</option>`).join('');
} catch (e) {
console.error('Failed to fetch servers:', e);
}
}
async function loadServerConfig() {
const guildId = document.getElementById('configServerSelect').value;
if (!guildId) {
document.getElementById('serverConfigPanel').style.display = 'none';
return;
}
try {
const response = await fetch(`/server-config/${guildId}`);
const data = await response.json();
currentServerConfig = data.config || {};
document.getElementById('configWelcomeChannel').value = currentServerConfig.welcome_channel || '';
document.getElementById('configGoodbyeChannel').value = currentServerConfig.goodbye_channel || '';
document.getElementById('configModlogChannel').value = currentServerConfig.modlog_channel || '';
document.getElementById('configLevelupChannel').value = currentServerConfig.level_up_channel || '';
document.getElementById('configAutoRole').value = currentServerConfig.auto_role || '';
document.getElementById('configVerifiedRole').value = currentServerConfig.verified_role || '';
document.getElementById('serverConfigPanel').style.display = 'block';
} catch (e) {
console.error('Failed to load server config:', e);
showToast('Failed to load configuration', 'error');
}
}
async function saveServerConfig() {
const guildId = document.getElementById('configServerSelect').value;
if (!guildId) return;
const config = {
guild_id: guildId,
welcome_channel: document.getElementById('configWelcomeChannel').value || null,
goodbye_channel: document.getElementById('configGoodbyeChannel').value || null,
modlog_channel: document.getElementById('configModlogChannel').value || null,
level_up_channel: document.getElementById('configLevelupChannel').value || null,
auto_role: document.getElementById('configAutoRole').value || null,
verified_role: document.getElementById('configVerifiedRole').value || null
};
try {
const token = localStorage.getItem('adminToken') || '';
const response = await fetch('/server-config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(config)
});
if (response.ok) {
showToast('Configuration saved successfully!', 'success');
} else {
throw new Error('Failed to save');
}
} catch (e) {
console.error('Failed to save config:', e);
showToast('Failed to save configuration', 'error');
}
}
async function fetchWhitelist() {
try {
const response = await fetch('/whitelist');
const data = await response.json();
const serversList = document.getElementById('whitelistServersList');
if (data.servers && data.servers.length > 0) {
serversList.innerHTML = data.servers.map(s => `
<div class="whitelist-item">
<div class="whitelist-info">
<div class="whitelist-icon">${(s.name || 'S').charAt(0).toUpperCase()}</div>
<div>
<div class="whitelist-name">${s.name || 'Unknown Server'}</div>
<div class="whitelist-id">${s.id}</div>
</div>
</div>
<span class="badge badge-success">Active</span>
</div>
`).join('');
} else {
serversList.innerHTML = '<div class="empty-state"><p>No whitelisted servers</p></div>';
}
const usersList = document.getElementById('whitelistUsersList');
if (data.users && data.users.length > 0) {
usersList.innerHTML = data.users.map(u => `
<div class="whitelist-item">
<div class="whitelist-info">
<div class="whitelist-icon">${(u.username || 'U').charAt(0).toUpperCase()}</div>
<div>
<div class="whitelist-name">${u.username || 'Unknown User'}</div>
<div class="whitelist-id">${u.id}</div>
</div>
</div>
<span class="badge badge-info">Heat Skip</span>
</div>
`).join('');
} else {
usersList.innerHTML = '<div class="empty-state"><p>No whitelisted users</p></div>';
}
} catch (e) {
console.error('Failed to fetch whitelist:', e);
document.getElementById('whitelistServersList').innerHTML = '<div class="empty-state"><p>Failed to load</p></div>';
document.getElementById('whitelistUsersList').innerHTML = '<div class="empty-state"><p>Failed to load</p></div>';
}
}
async function fetchRoles() {
try {
const response = await fetch('/roles');
const data = await response.json();
const levelRolesList = document.getElementById('levelRolesList');
if (data.levelRoles && data.levelRoles.length > 0) {
levelRolesList.innerHTML = data.levelRoles.map(r => `
<div class="role-item">
<div class="role-info">
<div class="role-color" style="background: ${r.color || 'var(--accent)'}"></div>
<div>
<div class="role-name">${r.name || 'Unknown Role'}</div>
<div class="role-meta">Level ${r.level} required</div>
</div>
</div>
<button class="btn btn-sm btn-secondary" onclick="removeLevelRole('${r.id}')">Remove</button>
</div>
`).join('');
} else {
levelRolesList.innerHTML = '<div class="empty-state"><p>No level roles configured</p></div>';
}
const federationRolesList = document.getElementById('federationRolesList');
if (data.federationRoles && data.federationRoles.length > 0) {
federationRolesList.innerHTML = data.federationRoles.map(r => `
<div class="role-item">
<div class="role-info">
<div class="role-color" style="background: ${r.color || 'var(--info)'}"></div>
<div>
<div class="role-name">${r.name || 'Unknown Role'}</div>
<div class="role-meta">${r.guildName || 'Unknown Server'}</div>
</div>
</div>
<span class="badge badge-info">Synced</span>
</div>
`).join('');
} else {
federationRolesList.innerHTML = '<div class="empty-state"><p>No federation roles linked</p></div>';
}
} catch (e) {
console.error('Failed to fetch roles:', e);
}
}
function showAddLevelRoleModal() {
showToast('Use /config levelrole command in Discord to add level roles', 'warning');
}
function removeLevelRole(roleId) {
showToast('Use /config remove-levelrole command in Discord to remove roles', 'warning');
}
async function populateAnnounceTargets() {
try {
const response = await fetch('/stats');
const data = await response.json();
const servers = data.guilds || [];
const targetsDiv = document.getElementById('announceTargets');
targetsDiv.innerHTML = `
<label class="checkbox-item">
<input type="checkbox" value="all" checked onchange="toggleAllServers(this)"> All Servers
</label>
${servers.map(s => `
<label class="checkbox-item">
<input type="checkbox" value="${s.id}" class="server-checkbox"> ${s.name}
</label>
`).join('')}
`;
} catch (e) {
console.error('Failed to populate announce targets:', e);
}
}
function toggleAllServers(checkbox) {
const serverCheckboxes = document.querySelectorAll('.server-checkbox');
serverCheckboxes.forEach(cb => {
cb.checked = checkbox.checked;
cb.disabled = checkbox.checked;
});
}
function clearAnnouncement() {
document.getElementById('announceTitle').value = '';
document.getElementById('announceMessage').value = '';
}
async function sendAnnouncement() {
const title = document.getElementById('announceTitle').value.trim();
const message = document.getElementById('announceMessage').value.trim();
if (!title || !message) {
showToast('Please fill in both title and message', 'warning');
return;
}
const allChecked = document.querySelector('input[value="all"]').checked;
let targets = [];
if (!allChecked) {
document.querySelectorAll('.server-checkbox:checked').forEach(cb => {
targets.push(cb.value);
});
if (targets.length === 0) {
showToast('Please select at least one server', 'warning');
return;
}
}
try {
const token = localStorage.getItem('adminToken') || '';
const response = await fetch('/announce', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
title,
message,
targets: allChecked ? 'all' : targets
})
});
if (response.ok) {
const result = await response.json();
showToast(`Announcement sent to ${result.sentCount || 0} servers!`, 'success');
clearAnnouncement();
fetchRecentAnnouncements();
} else {
throw new Error('Failed to send');
}
} catch (e) {
console.error('Failed to send announcement:', e);
showToast('Failed to send announcement', 'error');
}
}
async function fetchRecentAnnouncements() {
try {
const response = await fetch('/announcements');
const data = await response.json();
const container = document.getElementById('recentAnnouncements');
if (data.announcements && data.announcements.length > 0) {
container.innerHTML = data.announcements.slice(0, 5).map(a => `
<div class="announce-item">
<div class="announce-header">
<span class="announce-title">${a.title}</span>
<span class="announce-time">${formatTimeAgo(a.timestamp)}</span>
</div>
<div class="announce-message">${a.message}</div>
</div>
`).join('');
} else {
container.innerHTML = '<div class="empty-state"><p>No recent announcements</p></div>';
}
} catch (e) {
console.error('Failed to fetch announcements:', e);
}
}
// ============================================
// USER LOOKUP FUNCTIONS
// ============================================
let currentUserResults = [];
async function searchUser() {
const query = document.getElementById('userSearch').value.trim();
if (!query || query.length < 2) {
showToast('Please enter at least 2 characters', 'warning');
return;
}
document.getElementById('userSearchLoading').style.display = 'flex';
document.getElementById('userResult').style.display = 'none';
document.getElementById('userProfileCard').style.display = 'none';
try {
const response = await fetch(`/user-lookup/${encodeURIComponent(query)}`);
const data = await response.json();
currentUserResults = data.results || [];
document.getElementById('userSearchLoading').style.display = 'none';
document.getElementById('userResult').style.display = 'block';
if (currentUserResults.length === 0) {
document.getElementById('userResult').innerHTML = `
<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>No users found for "${query}"</p>
<p style="font-size: 0.8rem; margin-top: 0.5rem;">Try a Discord ID or different username</p>
</div>
`;
return;
}
if (currentUserResults.length === 1) {
showUserProfile(currentUserResults[0]);
} else {
document.getElementById('userResult').innerHTML = `
<p style="margin-bottom: 1rem; color: var(--text-muted);">Found ${currentUserResults.length} users:</p>
${currentUserResults.map((user, idx) => `
<div class="server-item" style="cursor: pointer;" onclick="showUserProfile(currentUserResults[${idx}])">
<div class="leaderboard-avatar">
${user.avatar ? `<img src="${user.avatar}" alt="">` : user.username.charAt(0).toUpperCase()}
</div>
<div class="server-info">
<div class="server-name">${user.username} ${user.bot ? '<span class="badge badge-info" style="margin-left: 0.5rem;">BOT</span>' : ''}</div>
<div class="server-members" style="font-family: monospace;">${user.id}</div>
</div>
<span class="badge ${user.linked ? 'badge-success' : 'badge-warning'}">${user.linked ? 'Linked' : 'Not Linked'}</span>
</div>
`).join('')}
`;
}
} catch (error) {
console.error('User search failed:', error);
document.getElementById('userSearchLoading').style.display = 'none';
document.getElementById('userResult').style.display = 'block';
document.getElementById('userResult').innerHTML = `
<div class="empty-state">
<p>Search failed. Please try again.</p>
</div>
`;
}
}
function showUserProfile(user) {
document.getElementById('userResult').innerHTML = `
<div class="empty-state">
<p>Profile shown below</p>
</div>
`;
document.getElementById('userProfileCard').style.display = 'block';
const heatClass = user.heat >= 3 ? 'danger' : user.heat >= 1 ? 'warning' : 'success';
const rolesHtml = user.roles && user.roles.length > 0
? user.roles.map(r => `<span class="badge" style="background: ${r.color}20; color: ${r.color}; margin-right: 0.25rem; margin-bottom: 0.25rem;">${r.name}</span>`).join('')
: '<span class="badge badge-warning">No roles</span>';
const serversHtml = user.servers && user.servers.length > 0
? user.servers.map(s => `<span class="badge badge-info" style="margin-right: 0.25rem;">${s.name}</span>`).join('')
: 'None';
const warningsHtml = user.warnings && user.warnings.length > 0
? user.warnings.map(w => `
<div class="activity-item">
<div class="activity-icon 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"/></svg>
</div>
<div class="activity-content">
<div class="activity-text">${w.reason || 'No reason provided'}</div>
<div class="activity-time">${formatTimeAgo(w.created_at)}</div>
</div>
</div>
`).join('')
: '<p style="color: var(--text-muted);">No warnings</p>';
const modHistoryHtml = user.modHistory && user.modHistory.length > 0
? user.modHistory.map(m => `
<div class="activity-item">
<div class="activity-icon stat-icon ${m.action === 'ban' ? 'red' : m.action === 'kick' ? 'purple' : 'blue'}">
<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="activity-content">
<div class="activity-text"><strong>${m.action.toUpperCase()}</strong> - ${m.reason || 'No reason'}</div>
<div class="activity-time">${formatTimeAgo(m.created_at)}</div>
</div>
</div>
`).join('')
: '<p style="color: var(--text-muted);">No mod actions</p>';
document.getElementById('userProfileContent').innerHTML = `
<div class="grid-2">
<div>
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem;">
<div class="leaderboard-avatar" style="width: 80px; height: 80px; font-size: 2rem;">
${user.avatar ? `<img src="${user.avatar}" alt="">` : user.username.charAt(0).toUpperCase()}
</div>
<div>
<h2 style="margin-bottom: 0.25rem;">${user.displayName || user.username}</h2>
<p style="color: var(--text-muted); font-size: 0.9rem;">@${user.username} ${user.bot ? '<span class="badge badge-info">BOT</span>' : ''}</p>
<p style="font-family: monospace; font-size: 0.8rem; color: var(--text-muted);">${user.id}</p>
</div>
</div>
<div class="stats-grid" style="grid-template-columns: repeat(3, 1fr); margin-bottom: 1.5rem;">
<div style="text-align: center; padding: 0.75rem; background: var(--bg-card); border-radius: 8px;">
<div style="font-size: 1.5rem; font-weight: 700; color: var(--accent);">${user.level || 0}</div>
<div style="font-size: 0.75rem; color: var(--text-muted);">Level</div>
</div>
<div style="text-align: center; padding: 0.75rem; background: var(--bg-card); border-radius: 8px;">
<div style="font-size: 1.5rem; font-weight: 700; color: var(--success);">${formatNumber(user.xp || 0)}</div>
<div style="font-size: 0.75rem; color: var(--text-muted);">XP</div>
</div>
<div style="text-align: center; padding: 0.75rem; background: var(--bg-card); border-radius: 8px;">
<div style="font-size: 1.5rem; font-weight: 700; color: var(--${heatClass});">${user.heat || 0}</div>
<div style="font-size: 0.75rem; color: var(--text-muted);">Heat</div>
</div>
</div>
<div style="margin-bottom: 1rem;">
<h4 style="margin-bottom: 0.5rem;">Account Status</h4>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<span class="badge ${user.linked ? 'badge-success' : 'badge-warning'}">${user.linked ? 'AeThex Linked' : 'Not Linked'}</span>
${user.realm ? `<span class="badge badge-info">${user.realm}</span>` : ''}
${user.dailyStreak ? `<span class="badge badge-success">${user.dailyStreak} day streak</span>` : ''}
</div>
</div>
<div style="margin-bottom: 1rem;">
<h4 style="margin-bottom: 0.5rem;">Servers</h4>
<div style="display: flex; gap: 0.25rem; flex-wrap: wrap;">${serversHtml}</div>
</div>
<div style="margin-bottom: 1rem;">
<h4 style="margin-bottom: 0.5rem;">Roles</h4>
<div style="display: flex; gap: 0.25rem; flex-wrap: wrap;">${rolesHtml}</div>
</div>
<div style="margin-bottom: 1rem;">
<h4 style="margin-bottom: 0.5rem;">Dates</h4>
<p style="font-size: 0.875rem;"><strong>Created:</strong> ${new Date(user.createdAt).toLocaleDateString()}</p>
${user.joinedAt ? `<p style="font-size: 0.875rem;"><strong>Joined:</strong> ${new Date(user.joinedAt).toLocaleDateString()}</p>` : ''}
</div>
</div>
<div>
<div class="card" style="margin-bottom: 1rem;">
<div class="card-header">
<h4 class="card-title" style="font-size: 1rem;">Warnings (${user.warnings?.length || 0})</h4>
</div>
<div class="card-body" style="max-height: 200px; overflow-y: auto;">
${warningsHtml}
</div>
</div>
<div class="card">
<div class="card-header">
<h4 class="card-title" style="font-size: 1rem;">Mod History</h4>
</div>
<div class="card-body" style="max-height: 200px; overflow-y: auto;">
${modHistoryHtml}
</div>
</div>
<div style="margin-top: 1rem;">
<h4 style="margin-bottom: 0.5rem;">Quick Actions</h4>
<p style="color: var(--text-muted); font-size: 0.875rem;">Use Discord commands for moderation:</p>
<div class="commands-grid" style="margin-top: 0.5rem; grid-template-columns: repeat(2, 1fr);">
<div class="command-item">warn ${user.id}</div>
<div class="command-item">kick ${user.id}</div>
<div class="command-item">timeout ${user.id}</div>
<div class="command-item">ban ${user.id}</div>
</div>
</div>
</div>
</div>
`;
}
function clearUserSearch() {
document.getElementById('userSearch').value = '';
document.getElementById('userProfileCard').style.display = 'none';
document.getElementById('userResult').innerHTML = `
<div class="empty-state">
<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>
<p>Search for a user by Discord ID or username</p>
<p style="font-size: 0.8rem; margin-top: 0.5rem;">View profile, XP, warnings, and mod history</p>
</div>
`;
currentUserResults = [];
}
// ============================================
// MODERATION FETCH
// ============================================
async function fetchModActions() {
try {
const response = await fetch('/mod-actions');
const data = await response.json();
if (data.counts) {
document.getElementById('warningCount').textContent = data.counts.warnings || 0;
document.getElementById('banCount').textContent = data.counts.bans || 0;
document.getElementById('timeoutCount').textContent = data.counts.timeouts || 0;
document.getElementById('kickCount').textContent = data.counts.kicks || 0;
}
const modLogContent = document.getElementById('modLogContent');
if (data.actions && data.actions.length > 0) {
modLogContent.innerHTML = data.actions.slice(0, 10).map(a => `
<div class="activity-item">
<div class="activity-icon stat-icon ${a.action === 'ban' ? 'red' : a.action === 'kick' ? 'purple' : a.action === 'warn' ? 'orange' : 'blue'}">
<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="activity-content">
<div class="activity-text"><strong>${a.action.toUpperCase()}</strong> ${a.user_tag || a.user_id} - ${a.reason || 'No reason'}</div>
<div class="activity-time">by ${a.moderator_tag || 'Unknown'} ${formatTimeAgo(a.created_at)}</div>
</div>
</div>
`).join('');
} else {
modLogContent.innerHTML = '<div class="empty-state"><p>No recent moderation actions</p></div>';
}
} catch (error) {
console.error('Failed to fetch mod actions:', error);
}
}
// ============================================
// INTEGRATIONS
// ============================================
async function fetchStudioFeed() {
try {
const response = await fetch('/studio-feed');
const data = await response.json();
const content = document.getElementById('studioFeedContent');
if (data.posts && data.posts.length > 0) {
content.innerHTML = data.posts.map(post => `
<div class="activity-item">
<div class="activity-icon stat-icon purple">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg>
</div>
<div class="activity-content">
<div class="activity-text"><strong>${post.author || 'Unknown'}</strong>: ${post.content || 'No content'}</div>
<div class="activity-time">${formatTimeAgo(post.created_at)} ${post.likes ? `| ${post.likes} likes` : ''}</div>
</div>
</div>
`).join('');
} else {
content.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg>
<p>${data.message || 'No posts available'}</p>
</div>
`;
}
document.getElementById('studioSync').textContent = new Date().toLocaleTimeString();
} catch (error) {
console.error('Failed to fetch studio feed:', error);
document.getElementById('studioFeedContent').innerHTML = '<div class="empty-state"><p>Failed to load Studio feed</p></div>';
}
}
async function fetchFoundationStats() {
try {
const response = await fetch('/foundation-stats');
const data = await response.json();
document.getElementById('foundationContributors').textContent = data.contributors || 0;
document.getElementById('foundationProjects').textContent = data.projects || 0;
document.getElementById('foundationCommits').textContent = data.commits || 0;
document.getElementById('foundationMembers').textContent = data.members || 0;
const activityEl = document.getElementById('foundationActivity');
if (data.activity && data.activity.length > 0) {
activityEl.innerHTML = data.activity.map(a => `
<div class="activity-item" style="padding: 0.5rem 0;">
<div class="activity-icon stat-icon ${a.type === 'commit' ? 'green' : a.type === 'pr' ? 'blue' : 'purple'}" style="width: 24px; height: 24px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 12px; height: 12px;">
${a.type === 'commit' ? '<circle cx="12" cy="12" r="4"/><line x1="1.05" y1="12" x2="7" y2="12"/><line x1="17.01" y1="12" x2="22.96" y2="12"/>' :
a.type === 'pr' ? '<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 0 0 9 9"/>' :
'<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>'}
</svg>
</div>
<div class="activity-content">
<div class="activity-text" style="font-size: 0.8rem;">${a.message || 'Activity'}</div>
<div class="activity-time">${a.author || 'Unknown'} ${formatTimeAgo(a.date)}</div>
</div>
</div>
`).join('');
} else {
activityEl.innerHTML = '<p style="color: var(--text-muted); font-size: 0.875rem;">No recent activity</p>';
}
document.getElementById('foundationSync').textContent = new Date().toLocaleTimeString();
} catch (error) {
console.error('Failed to fetch foundation stats:', error);
}
}
const webhookLogs = [];
async function testWebhook() {
const url = document.getElementById('webhookUrl').value.trim();
const message = document.getElementById('webhookMessage').value.trim();
const username = document.getElementById('webhookUsername').value.trim();
if (!url) {
showToast('Please enter a webhook URL', 'error');
return;
}
if (!message) {
showToast('Please enter a message', 'error');
return;
}
addWebhookLog('info', `Sending test to ${url.substring(0, 50)}...`);
try {
const response = await fetch('/test-webhook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, message, username: username || 'AeThex Dashboard' })
});
const data = await response.json();
if (data.success) {
addWebhookLog('success', `Message sent successfully!`);
showToast('Webhook test successful!', 'success');
} else {
addWebhookLog('error', `Failed: ${data.error || 'Unknown error'}`);
showToast('Webhook test failed: ' + (data.error || 'Unknown error'), 'error');
}
} catch (error) {
addWebhookLog('error', `Error: ${error.message}`);
showToast('Webhook test failed', 'error');
}
}
function addWebhookLog(type, message) {
const timestamp = new Date().toLocaleTimeString();
const color = type === 'success' ? 'var(--success)' : type === 'error' ? 'var(--danger)' : 'var(--info)';
webhookLogs.unshift({ timestamp, type, message, color });
if (webhookLogs.length > 20) webhookLogs.pop();
const logEl = document.getElementById('webhookTestLog');
logEl.innerHTML = webhookLogs.map(log =>
`<div style="margin-bottom: 0.5rem;"><span style="color: var(--text-muted);">[${log.timestamp}]</span> <span style="color: ${log.color};">${log.message}</span></div>`
).join('');
}
async function fetchIntegrationStatus() {
try {
const response = await fetch('/stats');
const data = await response.json();
if (data.bot) {
document.getElementById('discordApiSync').textContent = new Date().toLocaleTimeString();
const supabaseStatus = document.getElementById('supabaseStatus');
if (data.bot.supabaseConnected) {
supabaseStatus.textContent = 'Connected';
supabaseStatus.className = 'badge badge-success';
document.getElementById('supabaseSync').textContent = new Date().toLocaleTimeString();
document.getElementById('supabaseDetails').textContent = 'Database active';
} else {
supabaseStatus.textContent = 'Not Configured';
supabaseStatus.className = 'badge badge-warning';
document.getElementById('supabaseDetails').textContent = 'Using local storage';
}
const feedBridgeStatus = document.getElementById('feedBridgeStatus');
if (data.bot.feedBridgeActive) {
feedBridgeStatus.textContent = 'Active';
feedBridgeStatus.className = 'badge badge-success';
document.getElementById('feedBridgeSync').textContent = new Date().toLocaleTimeString();
document.getElementById('feedBridgeDetails').textContent = 'Syncing posts';
} else {
feedBridgeStatus.textContent = 'Inactive';
feedBridgeStatus.className = 'badge badge-warning';
document.getElementById('feedBridgeDetails').textContent = 'Not configured';
}
}
} catch (error) {
console.error('Failed to fetch integration status:', error);
}
}
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
${type === 'success' ? '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>' :
type === 'error' ? '<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>' :
'<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>
<span>${message}</span>
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideIn 0.3s ease reverse';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// =============================================================================
// WEBSOCKET CONNECTION FOR REAL-TIME UPDATES
// =============================================================================
let ws = null;
let wsReconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
const WS_RECONNECT_DELAY = 3000;
let wsConnected = false;
function initWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}`;
try {
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('[WebSocket] Connected');
wsConnected = true;
wsReconnectAttempts = 0;
updateConnectionStatus(true);
// Reduce polling interval since we have real-time updates
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = setInterval(refreshData, 60000); // Slower polling as backup
}
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
handleWebSocketMessage(message);
} catch (e) {
console.error('[WebSocket] Parse error:', e);
}
};
ws.onclose = () => {
console.log('[WebSocket] Disconnected');
wsConnected = false;
updateConnectionStatus(false);
// Immediately restore polling while disconnected
if (!refreshInterval) {
console.log('[WebSocket] Restoring polling while disconnected');
refreshInterval = setInterval(refreshData, 30000);
}
// Attempt reconnection with exponential backoff
if (wsReconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
wsReconnectAttempts++;
const backoffDelay = WS_RECONNECT_DELAY * Math.pow(1.5, wsReconnectAttempts - 1);
console.log(`[WebSocket] Reconnecting in ${Math.round(backoffDelay/1000)}s (attempt ${wsReconnectAttempts})...`);
setTimeout(initWebSocket, backoffDelay);
} else {
console.log('[WebSocket] Max reconnect attempts reached, continuing with polling');
}
};
ws.onerror = (error) => {
console.error('[WebSocket] Error:', error);
};
} catch (e) {
console.error('[WebSocket] Failed to connect:', e);
}
}
function handleWebSocketMessage(message) {
switch (message.type) {
case 'stats':
updateStatsFromWebSocket(message.data);
break;
case 'activity':
addActivityFromWebSocket(message.data);
break;
case 'command':
updateCommandFromWebSocket(message.data);
break;
case 'threat':
addThreatFromWebSocket(message.data);
break;
case 'ticket':
updateTicketFromWebSocket(message.data);
break;
default:
console.log('[WebSocket] Unknown message type:', message.type);
}
}
function updateStatsFromWebSocket(data) {
if (data.guilds !== undefined) {
document.getElementById('serverCount').textContent = data.guilds;
}
if (data.totalMembers !== undefined) {
document.getElementById('memberCount').textContent = formatNumber(data.totalMembers);
}
if (data.uptime !== undefined) {
document.getElementById('uptime').textContent = formatUptime(data.uptime);
document.getElementById('systemUptime').textContent = formatUptime(data.uptime);
}
if (data.heatMapSize !== undefined) {
document.getElementById('heatMapSize').textContent = data.heatMapSize;
document.getElementById('sentinelHeatMap').textContent = data.heatMapSize;
}
if (data.activeTickets !== undefined) {
document.getElementById('activeTickets').textContent = data.activeTickets;
document.getElementById('ticketBadge').textContent = data.activeTickets;
}
if (data.federationLinks !== undefined) {
document.getElementById('federationCount').textContent = data.federationLinks;
document.getElementById('sentinelFederation').textContent = data.federationLinks;
}
if (data.commandsExecuted !== undefined) {
document.getElementById('commandCount').textContent = data.commandsExecuted;
}
// Update last update time
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
}
function addActivityFromWebSocket(data) {
const activityFeed = document.getElementById('activityFeed');
if (!activityFeed) return;
const iconClass = data.type === 'command' ? 'purple' :
data.type === 'moderation' ? 'red' :
data.type === 'member' ? 'green' : 'blue';
const iconSvg = data.type === 'command' ? '<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/>' :
data.type === 'moderation' ? '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>' :
data.type === 'member' ? '<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><path d="M20 8v6"/><path d="M23 11h-6"/>' :
'<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>';
const newActivity = document.createElement('div');
newActivity.className = 'activity-item';
newActivity.innerHTML = `
<div class="activity-icon stat-icon ${iconClass}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">${iconSvg}</svg>
</div>
<div class="activity-content">
<div class="activity-text">${data.message || 'Activity'}</div>
<div class="activity-time">Just now</div>
</div>
`;
// Add animation
newActivity.style.animation = 'slideIn 0.3s ease';
// Insert at top
if (activityFeed.firstChild) {
activityFeed.insertBefore(newActivity, activityFeed.firstChild);
} else {
activityFeed.appendChild(newActivity);
}
// Limit to 20 items
while (activityFeed.children.length > 20) {
activityFeed.removeChild(activityFeed.lastChild);
}
}
function updateCommandFromWebSocket(data) {
// Update command analytics if on that section
if (data.commandName && data.success !== undefined) {
// Could update command usage chart here
console.log(`[WebSocket] Command executed: ${data.commandName}`);
}
}
function addThreatFromWebSocket(data) {
const threatFeed = document.getElementById('threatsFeed');
if (!threatFeed) return;
const levelClass = data.level === 'critical' ? 'danger' :
data.level === 'high' ? 'warning' : 'info';
const newThreat = document.createElement('div');
newThreat.className = 'activity-item';
newThreat.innerHTML = `
<div class="activity-icon stat-icon ${levelClass === 'danger' ? 'red' : levelClass === 'warning' ? '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">${data.message || 'Security alert'}</div>
<div class="activity-time">Just now - <span class="badge badge-${levelClass}">${data.level || 'unknown'}</span></div>
</div>
`;
newThreat.style.animation = 'slideIn 0.3s ease';
if (threatFeed.firstChild) {
threatFeed.insertBefore(newThreat, threatFeed.firstChild);
} else {
threatFeed.appendChild(newThreat);
}
// Update threat badge
const badge = document.getElementById('threatBadge');
if (badge) {
badge.textContent = parseInt(badge.textContent || 0) + 1;
}
}
function updateTicketFromWebSocket(data) {
if (data.action === 'create') {
const badge = document.getElementById('ticketBadge');
if (badge) {
badge.textContent = parseInt(badge.textContent || 0) + 1;
}
document.getElementById('activeTickets').textContent =
parseInt(document.getElementById('activeTickets').textContent || 0) + 1;
} else if (data.action === 'close') {
const badge = document.getElementById('ticketBadge');
if (badge) {
const current = parseInt(badge.textContent || 0);
badge.textContent = Math.max(0, current - 1);
}
const activeEl = document.getElementById('activeTickets');
activeEl.textContent = Math.max(0, parseInt(activeEl.textContent || 0) - 1);
}
}
function updateConnectionStatus(connected) {
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
if (connected) {
// Add WebSocket indicator
if (!document.getElementById('wsIndicator')) {
const indicator = document.createElement('span');
indicator.id = 'wsIndicator';
indicator.style.cssText = 'margin-left: 8px; font-size: 0.7rem; color: var(--success);';
indicator.textContent = '(Live)';
statusText.parentNode.appendChild(indicator);
}
} else {
const indicator = document.getElementById('wsIndicator');
if (indicator) indicator.remove();
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
// Load theme
const savedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
// Initialize WebSocket connection
initWebSocket();
// Initial fetch
refreshData();
fetchExtendedStatus();
fetchStudioFeed();
fetchFoundationStats();
fetchIntegrationStatus();
// Auto-refresh every 30s (will be reduced when WebSocket connects)
refreshInterval = setInterval(refreshData, 30000);
});
</script>
</body>
</html>