From 8047bce755cf6e9fb494bbc509d72b7837853754 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Wed, 7 Jan 2026 14:26:37 +0100 Subject: [PATCH] feat: add bot action controls and real-time vital statistics to the web dashboard --- src/web/public/script.js | 144 ++++++- src/web/public/style.css | 810 +++++++++++++----------------------- src/web/router.ts | 7 + src/web/routes/actions.ts | 56 +++ src/web/routes/dashboard.ts | 123 ++++-- src/web/server.ts | 13 +- src/web/views/layout.ts | 17 +- 7 files changed, 588 insertions(+), 582 deletions(-) create mode 100644 src/web/routes/actions.ts diff --git a/src/web/public/script.js b/src/web/public/script.js index 26d3872..51d88fa 100644 --- a/src/web/public/script.js +++ b/src/web/public/script.js @@ -16,19 +16,24 @@ function formatUptime(seconds) { } function updateUptime() { - const el = document.getElementById("uptime-display"); - if (!el) return; + const elements = document.querySelectorAll(".uptime-display, #uptime-display"); + elements.forEach(el => { + const startTimestamp = parseInt(el.getAttribute("data-start-timestamp"), 10); + if (isNaN(startTimestamp)) return; - const startTimestamp = parseInt(el.getAttribute("data-start-timestamp"), 10); - if (isNaN(startTimestamp)) return; + const now = Date.now(); + const elapsedSeconds = (now - startTimestamp) / 1000; - const now = Date.now(); - const elapsedSeconds = (now - startTimestamp) / 1000; - - el.textContent = formatUptime(elapsedSeconds); + el.textContent = formatUptime(elapsedSeconds); + }); } document.addEventListener("DOMContentLoaded", () => { + // Initialize Lucide Icons + if (window.lucide) { + window.lucide.createIcons(); + } + // Update immediately to prevent stale content flash if possible updateUptime(); // Update every second @@ -51,9 +56,7 @@ document.addEventListener("DOMContentLoaded", () => { try { const msg = JSON.parse(event.data); if (msg.type === "HEARTBEAT") { - console.log("Heartbeat:", msg.data); - // Sync uptime? - // We can optionally verify if client clock is drifting, but let's keep it simple. + updateVitals(msg.data); } else if (msg.type === "WELCOME") { console.log(msg.message); } else if (msg.type === "LOG") { @@ -64,12 +67,74 @@ document.addEventListener("DOMContentLoaded", () => { } }; + function updateVitals(data) { + // Update Stats + if (data.guildCount !== undefined) { + const el = document.getElementById("stat-servers"); + if (el) el.textContent = data.guildCount; + } + if (data.userCount !== undefined) { + const el = document.getElementById("stat-users"); + if (el) el.textContent = data.userCount; + } + if (data.commandCount !== undefined) { + const el = document.getElementById("stat-commands"); + if (el) el.textContent = data.commandCount; + } + if (data.ping !== undefined) { + const el = document.getElementById("stat-ping"); + if (el) el.textContent = `${data.ping < 0 ? "?" : data.ping}ms`; + + const trend = document.getElementById("stat-ping-trend"); + if (trend) { + trend.className = `stat-trend ${data.ping < 100 ? "up" : "down"}`; + const icon = trend.querySelector('i'); + if (icon) { + icon.setAttribute('data-lucide', data.ping < 100 ? "check-circle" : "alert-circle"); + if (window.lucide) window.lucide.createIcons(); + } + const text = trend.querySelector('span'); + if (text) text.textContent = data.ping < 100 ? "Excellent" : "Decent"; + } + } + + // Update System Health + if (data.memory !== undefined) { + const el = document.getElementById("stat-memory"); + if (el && data.memoryTotal) { + el.textContent = `${data.memory} / ${data.memoryTotal} MB`; + } else if (el) { + el.textContent = `${data.memory} MB`; + } + + const bar = document.getElementById("stat-memory-bar"); + if (bar && data.memoryTotal) { + const percent = Math.min(100, (data.memory / data.memoryTotal) * 100); + bar.style.width = `${percent}%`; + } + } + + if (data.uptime !== undefined) { + // We handle uptime fluidly in updateUptime() using data-start-timestamp. + // We just ensure the attribute is kept in sync if needed (though startTimestamp shouldn't change). + const elements = document.querySelectorAll(".uptime-display, #uptime-display"); + elements.forEach(el => { + const currentStart = parseInt(el.getAttribute("data-start-timestamp"), 10); + const newStart = Math.floor(Date.now() - (data.uptime * 1000)); + // Only update if there's a significant drift (> 5s) + if (isNaN(currentStart) || Math.abs(currentStart - newStart) > 5000) { + el.setAttribute("data-start-timestamp", newStart); + } + }); + } + } + function appendToActivityFeed(log) { const list = document.querySelector(".activity-feed"); if (!list) return; const item = document.createElement("li"); - item.className = `activity-item ${log.type}`; + item.className = "activity-item"; const timeSpan = document.createElement("span"); timeSpan.className = "time"; @@ -82,13 +147,9 @@ document.addEventListener("DOMContentLoaded", () => { item.appendChild(timeSpan); item.appendChild(messageSpan); - // Prepend to top + // Prepend list.insertBefore(item, list.firstChild); - - // Limit history - if (list.children.length > 50) { - list.removeChild(list.lastChild); - } + if (list.children.length > 50) list.lastChild.remove(); } ws.onclose = () => { @@ -104,5 +165,52 @@ document.addEventListener("DOMContentLoaded", () => { }; } + // Action Buttons + document.querySelectorAll(".btn[data-action]").forEach(btn => { + btn.addEventListener("click", async () => { + const action = btn.getAttribute("data-action"); + const actionName = btn.textContent.trim(); + + if (action === "restart_bot") { + if (!confirm("Are you sure you want to restart the bot? This will cause a brief downtime.")) { + return; + } + } + + btn.disabled = true; + const originalText = btn.textContent; + btn.textContent = "Processing..."; + + try { + const response = await fetch("/api/actions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action }) + }); + + const result = await response.json(); + if (result.success) { + alert(`${actionName} successful: ${result.message}`); + if (action === "restart_bot") { + btn.textContent = "Restarting..."; + setTimeout(() => window.location.reload(), 5000); + } else { + btn.disabled = false; + btn.textContent = originalText; + } + } else { + alert(`Error: ${result.error}`); + btn.disabled = false; + btn.textContent = originalText; + } + } catch (err) { + console.error("Action error:", err); + alert("Failed to execute action. Check console."); + btn.disabled = false; + btn.textContent = originalText; + } + }); + }); + connectWs(); }); diff --git a/src/web/public/style.css b/src/web/public/style.css index 0d064da..e8ca21f 100644 --- a/src/web/public/style.css +++ b/src/web/public/style.css @@ -1,525 +1,203 @@ :root { - /* Color Palette - HSL (Hue, Saturation, Lightness) */ - /* Primary (Aurora Cyan) */ - --primary-h: 180; - --primary-s: 100%; - --primary-l: 50%; - --primary: hsl(var(--primary-h), var(--primary-s), var(--primary-l)); + /* Geist Inspired Minimal Palette */ + --background: #000; + --foreground: #fff; + --accents-1: #111; + --accents-2: #333; + --accents-3: #444; + --accents-4: #666; + --accents-5: #888; + --accents-6: #999; + --accents-7: #eaeaea; + --accents-8: #fafafa; - /* Secondary (Aurora Purple) */ - --secondary-h: 270; - --secondary-s: 100%; - --secondary-l: 65%; - --secondary: hsl(var(--secondary-h), var(--secondary-s), var(--secondary-l)); + --success: #0070f3; + --success-light: #3291ff; + --error: #ee0000; + --error-light: #ff1a1a; + --warning: #f5a623; + --warning-light: #f7b955; - /* Backgrounds (Dark Slate) */ - --bg-h: 222; - --bg-s: 47%; - --bg-l: 7%; - /* Very Dark */ - --bg-color: hsl(var(--bg-h), var(--bg-s), var(--bg-l)); + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + --font-mono: 'Menlo', 'Monaco', 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', monospace; - --card-bg-h: 217; - --card-bg-s: 33%; - --card-bg-l: 15%; - --card-bg: hsl(var(--card-bg-h), var(--card-bg-s), var(--card-bg-l)); - - /* Text */ - --text-main: hsl(210, 40%, 98%); - --text-muted: hsl(215, 20%, 65%); - --text-accent: var(--primary); - - /* Borders */ - --border-color: hsl(215, 25%, 25%); - - /* Typography */ - --font-heading: 'Outfit', system-ui, sans-serif; - --font-body: 'Inter', system-ui, sans-serif; - - /* Spacing & Radii */ - --radius-md: 0.75rem; - --radius-lg: 1rem; - --header-height: 4rem; - - /* Effects */ - --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.2), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --shadow-glow: 0 0 15px hsla(var(--primary-h), var(--primary-s), 50%, 0.15); + --radius: 5px; + --header-height: 64px; + --sidebar-width: 240px; } -*, -*::before, -*::after { +* { box-sizing: border-box; } body { - background-color: var(--bg-color); - color: var(--text-main); - font-family: var(--font-body); + background: var(--background); + color: var(--foreground); + font-family: var(--font-sans); margin: 0; - line-height: 1.6; - display: flex; - flex-direction: column; - min-height: 100vh; -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-size: 14px; } +/* Typography */ h1, h2, h3, -h4, -h5, -h6 { - font-family: var(--font-heading); - margin-top: 0; - line-height: 1.2; - color: var(--text-main); +h4 { + font-weight: 600; + margin: 0; + color: var(--foreground); } h1 { - font-weight: 700; + font-size: 2rem; + letter-spacing: -0.05rem; +} + +h2 { + font-size: 1.5rem; + letter-spacing: -0.02rem; +} + +h3 { + font-size: 1rem; +} + +a { + color: var(--accents-5); + text-decoration: none; + transition: color 0.2s ease; +} + +a:hover { + color: var(--foreground); } /* Header */ header { - background: rgba(15, 23, 42, 0.8); - /* Semi-transparent */ - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border-bottom: 1px solid var(--border-color); height: var(--header-height); - padding: 0 2rem; + border-bottom: 1px solid var(--accents-2); display: flex; - justify-content: space-between; align-items: center; + padding: 0 24px; position: sticky; top: 0; - z-index: 50; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: saturate(180%) blur(5px); + z-index: 1000; } header h1 { - font-size: 1.5rem; - margin: 0; - background: linear-gradient(135deg, var(--primary), var(--secondary)); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - letter-spacing: -0.02em; + font-size: 1.25rem; + font-weight: 700; +} + +header nav { + margin-left: 24px; + display: flex; + gap: 16px; } header nav a { - color: var(--text-muted); - text-decoration: none; - font-weight: 500; - margin-left: 1.5rem; - transition: color 0.15s ease; - font-size: 0.95rem; -} - -header nav a:hover { - color: var(--primary); -} - -/* Main Layout */ -main { - flex: 1; - padding: 2rem; - max-width: 1200px; - margin: 0 auto; - width: 100%; -} - -/* Card Component */ -.card { - background-color: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - padding: 2rem; - margin-bottom: 1.5rem; - box-shadow: var(--shadow-md); - position: relative; - overflow: hidden; - transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; -} - -.card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-glow), var(--shadow-md); - border-color: hsla(var(--primary-h), var(--primary-s), 50%, 0.3); -} - -.card h2 { - font-size: 1.25rem; - margin-bottom: 1rem; - color: var(--text-main); - display: flex; - align-items: center; - gap: 0.5rem; -} - -.card p { - color: var(--text-muted); - margin-bottom: 0; - font-size: 0.95rem; -} - -/* Links */ -a { - color: var(--primary); - text-decoration: none; - transition: opacity 0.2s; -} - -a:hover { - opacity: 0.8; -} - -/* Buttons (Future Proofing) */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.5rem 1rem; - border-radius: var(--radius-md); - font-weight: 600; - font-family: var(--font-heading); - cursor: pointer; - transition: all 0.2s ease; - border: none; - font-size: 0.9rem; - text-decoration: none; -} - -.btn-primary { - background: linear-gradient(135deg, var(--primary), hsl(var(--primary-h), 90%, 45%)); - color: #000; - /* Contrast text on Cyan */ - box-shadow: 0 4px 6px -1px hsla(var(--primary-h), var(--primary-s), 50%, 0.2); -} - -.btn-primary:hover { - filter: brightness(1.1); - box-shadow: 0 6px 8px -1px hsla(var(--primary-h), var(--primary-s), 50%, 0.3); -} - -/* Forms & Inputs */ -input[type="text"], -input[type="email"], -input[type="password"], -textarea, -select { - width: 100%; - padding: 0.75rem 1rem; - background-color: rgba(15, 23, 42, 0.5); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - color: var(--text-main); - font-family: var(--font-body); - font-size: 0.95rem; - transition: all 0.2s; -} - -input:focus, -textarea:focus, -select:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 2px hsla(var(--primary-h), var(--primary-s), 50%, 0.2); - background-color: rgba(15, 23, 42, 0.8); -} - -/* Tables */ -table { - width: 100%; - border-collapse: collapse; - margin: 1rem 0; -} - -th { - text-align: left; - padding: 1rem; - background-color: rgba(15, 23, 42, 0.5); - color: var(--text-muted); - font-weight: 600; - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 0.05em; - border-bottom: 1px solid var(--border-color); -} - -td { - padding: 1rem; - border-bottom: 1px solid #1e293b; - /* Fallback or specific border */ - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - color: var(--text-main); -} - -tr:last-child td { - border-bottom: none; -} - -tr:hover td { - background-color: rgba(255, 255, 255, 0.02); -} - -/* Footer */ -footer { - padding: 2rem; - text-align: center; - color: var(--text-muted); font-size: 0.875rem; - border-top: 1px solid var(--border-color); - background: var(--bg-color); } -/* Utilities */ -.text-gradient { - background: linear-gradient(135deg, var(--primary), var(--secondary)); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; +header nav a.active { + color: var(--foreground); } -/* Animations & Micro-Interactions */ -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - - to { - opacity: 1; - transform: translateY(0); - } +/* Layout */ +main { + max-width: 1000px; + margin: 0 auto; + padding: 48px 24px; } -@keyframes slideUp { - from { - opacity: 0; - transform: translateY(20px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -/* Entry Animations */ -.fade-in { - animation: fadeIn 0.4s ease-out forwards; -} - -/* Stagger animations for children using nth-child */ -main>* { - opacity: 0; - /* Initially hidden */ - animation: slideUp 0.5s ease-out forwards; -} - -main>*:nth-child(1) { - animation-delay: 0.1s; -} - -main>*:nth-child(2) { - animation-delay: 0.2s; -} - -main>*:nth-child(3) { - animation-delay: 0.3s; -} - -main>*:nth-child(4) { - animation-delay: 0.4s; -} - -/* Dynamic Background */ -body::before { - content: ''; - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: - radial-gradient(circle at 15% 50%, hsla(var(--primary-h), var(--primary-s), var(--primary-l), 0.08), transparent 25%), - radial-gradient(circle at 85% 30%, hsla(var(--secondary-h), var(--secondary-s), var(--secondary-l), 0.08), transparent 25%); - z-index: -1; - pointer-events: none; -} - -/* Link Interactions */ -a { - position: relative; - transition: color 0.2s ease, opacity 0.2s ease; -} - -header nav a::after { - content: ''; - position: absolute; - bottom: -4px; - left: 0; - width: 0%; - height: 2px; - background: var(--primary); - transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -header nav a:hover::after { - width: 100%; -} - -/* Accessibility: Reduced Motion */ -@media (prefers-reduced-motion: reduce) { - - *, - *::before, - *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - scroll-behavior: auto !important; - } -} - -/* Mobile Responsiveness */ -@media (max-width: 768px) { - :root { - --header-height: 3.5rem; - /* Compact header on mobile */ - } - - body { - font-size: 14px; - /* Slightly smaller base font */ - } - - /* Layout Adjustments */ - header { - padding: 0 1rem; - } - - header nav a { - margin-left: 1rem; - font-size: 0.9rem; - } - - main { - padding: 1rem; - width: 100%; - max-width: 100%; - } - - /* Typography Scaling */ - h1 { - font-size: 1.75rem; - } - - h2 { - font-size: 1.5rem; - } - - h3 { - font-size: 1.25rem; - } - - /* Card Adjustments */ - .card { - padding: 1.25rem; - border-radius: var(--radius-md); - /* Slightly smaller radius */ - } - - /* Stack flex containers if needed (general util) */ - .flex-col-mobile { - flex-direction: column !important; - } - - /* Touch Targets */ - .btn, - a, - input, - select { - min-height: 44px; - /* Compliance with touch target guidelines */ - } - - /* Horizontal scroll for wide tables */ - .table-container { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - margin-left: -1rem; - margin-right: -1rem; - padding-left: 1rem; - padding-right: 1rem; - } -} -/* Dashboard Layout */ +/* Dashboard Grid */ .dashboard-grid { display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 1.5rem; - margin-bottom: 2rem; + grid-template-columns: repeat(3, 1fr); + gap: 24px; +} + +/* Cards */ +.card, +.panel, +.stat-card { + background: var(--background); + border: 1px solid var(--accents-2); + border-radius: var(--radius); + padding: 24px; + transition: border-color 0.2s ease; +} + +.card:hover, +.panel:hover, +.stat-card:hover { + border-color: var(--accents-4); } .stat-card { - background: var(--card-bg); - border: 1px solid var(--border-color); - padding: 1.5rem; - border-radius: var(--radius-lg); - text-align: center; - box-shadow: var(--shadow-sm); -} - -.stat-card h3 { - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted); - margin-bottom: 0.5rem; -} - -.stat-card .stat-value { - font-size: 2rem; - font-weight: 700; - color: var(--text-main); - font-family: var(--font-heading); -} - -.dashboard-main { - grid-column: 1 / -1; - display: grid; - grid-template-columns: 2fr 1fr; - gap: 1.5rem; -} - -.panel { - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - padding: 1.5rem; display: flex; flex-direction: column; + justify-content: space-between; + min-height: 120px; } -.panel.control-panel { - grid-column: 1 / -1; -} - -.panel-header { +.stat-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 1rem; - border-bottom: 1px solid var(--border-color); - padding-bottom: 0.75rem; + margin-bottom: 8px; +} + +.stat-header h3 { + font-size: 0.75rem; + text-transform: uppercase; + color: var(--accents-5); + letter-spacing: 1px; +} + +.stat-value { + font-size: 2rem; + font-weight: 700; + letter-spacing: -1px; +} + +.stat-trend { + font-size: 0.75rem; + margin-top: 8px; + color: var(--accents-5); + display: flex; + align-items: center; + gap: 4px; +} + +.stat-trend.up { + color: var(--success); +} + +.stat-trend.down { + color: var(--error); +} + +/* Panels */ +.dashboard-main { + grid-column: 1 / -1; + display: grid; + grid-template-columns: 1.5fr 1fr; + gap: 24px; + margin-top: 24px; +} + +.panel-header { + margin-bottom: 20px; } .panel-header h2 { font-size: 1.1rem; - margin: 0; + font-weight: 600; } /* Activity Feed */ @@ -527,81 +205,181 @@ header nav a:hover::after { list-style: none; padding: 0; margin: 0; - max-height: 300px; + max-height: 400px; overflow-y: auto; } .activity-item { - display: flex; - gap: 1rem; - padding: 0.75rem 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - font-size: 0.9rem; + padding: 12px 0; + border-bottom: 1px solid var(--accents-2); + font-size: 0.875rem; +} + +.activity-item:last-child { + border-bottom: none; } .activity-item .time { - color: var(--text-muted); - font-family: monospace; + color: var(--accents-4); + font-family: var(--font-mono); + font-size: 0.75rem; + display: block; + margin-bottom: 4px; } -.activity-item.info .message { color: var(--text-main); } -.activity-item.success .message { color: hsl(150, 60%, 45%); } -.activity-item.warning .message { color: hsl(35, 90%, 60%); } -.activity-item.error .message { color: hsl(0, 80%, 60%); } +/* Badges */ +.badge { + padding: 2px 8px; + border-radius: 20px; + font-size: 0.65rem; + font-weight: 600; + text-transform: uppercase; +} .badge.live { - background: hsla(0, 100%, 50%, 0.2); - color: hsl(0, 100%, 60%); - padding: 0.25rem 0.5rem; - border-radius: 4px; - font-size: 0.7rem; - font-weight: bold; - text-transform: uppercase; - animation: pulse 2s infinite; + background: rgba(0, 112, 243, 0.1); + color: var(--success); + border: 1px solid rgba(0, 112, 243, 0.2); } -@keyframes pulse { - 0% { opacity: 1; } - 50% { opacity: 0.5; } - 100% { opacity: 1; } -} - -/* Mock Chart */ -.mock-chart-container { - height: 200px; +/* System Metrics */ +.metrics-grid { display: flex; - align-items: flex-end; - gap: 4px; - padding-top: 1rem; - border-bottom: 1px solid var(--border-color); - margin-bottom: 0.5rem; + flex-direction: column; + gap: 16px; } -.mock-chart-bar { - flex: 1; - background: var(--primary); - opacity: 0.5; - border-radius: 2px 2px 0 0; - transition: height 0.5s ease; +.metric-card { + padding: 12px; + border: 1px solid var(--accents-2); + border-radius: var(--radius); } -.mock-chart-bar:hover { - opacity: 0.8; -} - -.metrics-legend { +.metric-header { + display: flex; + justify-content: space-between; font-size: 0.8rem; - color: var(--text-muted); - text-align: center; + margin-bottom: 8px; } -/* Responsive Dashboard */ +.metric-label { + color: var(--accents-5); +} + +.metric-value { + font-weight: 500; + font-family: var(--font-mono); +} + +.progress-bar-bg { + height: 4px; + background: var(--accents-2); + border-radius: 2px; + overflow: hidden; +} + +.progress-bar-fill { + height: 100%; + background: var(--foreground); + transition: width 0.3s ease; +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 16px; + height: 32px; + border-radius: var(--radius); + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid var(--accents-2); + background: var(--background); + color: var(--foreground); +} + +.btn:hover:not(:disabled) { + border-color: var(--foreground); +} + +.btn-primary { + background: var(--foreground); + color: var(--background); + border: 1px solid var(--foreground); +} + +.btn-primary:hover:not(:disabled) { + background: var(--background); + color: var(--foreground); +} + +.btn-danger { + color: var(--error); + border-color: var(--error); +} + +.btn-danger:hover:not(:disabled) { + background: var(--error); + color: white; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Control Panel */ +.control-panel { + grid-column: 1 / -1; + margin-top: 24px; +} + +.action-buttons { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +/* Footer */ +footer { + padding: 48px 24px; + border-top: 1px solid var(--accents-2); + color: var(--accents-5); + font-size: 0.8rem; +} + +.footer-content { + max-width: 1000px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; +} + +.status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + background: var(--accents-4); + margin-right: 8px; +} + +.status-indicator.online { + background: var(--success); + box-shadow: 0 0 8px var(--success); +} + +/* Responsive */ @media (max-width: 768px) { .dashboard-grid { - grid-template-columns: 1fr 1fr; /* 2 columns on tablet/mobile */ + grid-template-columns: 1fr; } - + .dashboard-main { - grid-template-columns: 1fr; /* Stack panels */ + grid-template-columns: 1fr; } -} +} \ No newline at end of file diff --git a/src/web/router.ts b/src/web/router.ts index 82ed695..7ba3e28 100644 --- a/src/web/router.ts +++ b/src/web/router.ts @@ -45,5 +45,12 @@ export async function router(request: Request): Promise { } } + if (method === "POST") { + if (url.pathname === "/api/actions") { + const { actionsRoute } = await import("./routes/actions"); + return actionsRoute(request); + } + } + return new Response("Not Found", { status: 404 }); } diff --git a/src/web/routes/actions.ts b/src/web/routes/actions.ts new file mode 100644 index 0000000..d529c8b --- /dev/null +++ b/src/web/routes/actions.ts @@ -0,0 +1,56 @@ +import { AuroraClient } from "@/lib/BotClient"; +import { logger } from "@/lib/logger"; + +export async function actionsRoute(request: Request): Promise { + const url = new URL(request.url); + const body = await request.json().catch(() => ({})) as any; + const action = body.action; + + if (!action) { + return new Response(JSON.stringify({ success: false, error: "No action provided" }), { + status: 400, + headers: { "Content-Type": "application/json" } + }); + } + + try { + switch (action) { + case "reload_commands": + logger.info("Web Dashboard: Triggering command reload..."); + await AuroraClient.loadCommands(true); + await AuroraClient.deployCommands(); + return new Response(JSON.stringify({ success: true, message: "Commands reloaded successfully" }), { + headers: { "Content-Type": "application/json" } + }); + + case "clear_cache": + logger.info("Web Dashboard: Triggering cache clear..."); + // For now, we'll reload events and commands as a "clear cache" action + await AuroraClient.loadEvents(true); + await AuroraClient.loadCommands(true); + return new Response(JSON.stringify({ success: true, message: "Cache cleared and systems reloaded" }), { + headers: { "Content-Type": "application/json" } + }); + + case "restart_bot": + logger.info("Web Dashboard: Triggering bot restart..."); + // We don't await this because it will exit the process + setTimeout(() => AuroraClient.shutdown(), 1000); + return new Response(JSON.stringify({ success: true, message: "Bot shutdown initiated. If managed by a process manager, it will restart." }), { + headers: { "Content-Type": "application/json" } + }); + + default: + return new Response(JSON.stringify({ success: false, error: `Unknown action: ${action}` }), { + status: 400, + headers: { "Content-Type": "application/json" } + }); + } + } catch (error: any) { + logger.error(`Error executing action ${action}:`, error); + return new Response(JSON.stringify({ success: false, error: error.message }), { + status: 500, + headers: { "Content-Type": "application/json" } + }); + } +} diff --git a/src/web/routes/dashboard.ts b/src/web/routes/dashboard.ts index e7141e8..b167bc3 100644 --- a/src/web/routes/dashboard.ts +++ b/src/web/routes/dashboard.ts @@ -12,72 +12,115 @@ export function dashboardRoute(): Response { const ping = AuroraClient.ws.ping; // Real system metrics - const memoryUsage = (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2); + const memUsage = process.memoryUsage(); + const memoryUsage = (memUsage.heapUsed / 1024 / 1024).toFixed(2); + const memoryTotal = (memUsage.rss / 1024 / 1024).toFixed(1); const uptimeSeconds = process.uptime(); const uptime = new Date(uptimeSeconds * 1000).toISOString().substr(11, 8); // HH:MM:SS + const startTimestamp = Date.now() - (uptimeSeconds * 1000); // Real activity logs const activityLogs = getRecentLogs(); + const memPercent = Math.min(100, (memUsage.heapUsed / memUsage.rss) * 100).toFixed(1); + + // Get top guilds + const topGuilds = AuroraClient.guilds.cache + .sort((a, b) => b.memberCount - a.memberCount) + .first(5); + const content = `
-

Servers

-
${guildCount}
+
+

Members

+ +
+
${userCount.toLocaleString()}
+
+ Total user reach +
-

Users

-
${userCount}
+
+

Guilds

+ +
+
${guildCount}
+
+ Active connections +
-

Commands

-
${commandCount}
-
-
-

Ping

-
${ping < 0 ? "?" : ping}ms
+
+

Latency

+ +
+
${ping < 0 ? "?" : ping}ms
+
+ + ${ping < 100 ? "Stable" : "High"} +
-
-

Live Activity

- LIVE +
+

Activity Flow

+ Live
    ${activityLogs.length > 0 ? activityLogs.map(log => ` -
  • +
  • ${log.time} ${log.message}
  • `).join('') : ` -
  • --:--:-- No recent activity.
  • +
  • + Listening for activity... +
  • `}
-
-
-

System Health

+
+
+
+

Health

+
+
+
+
+ Memory + ${memoryUsage} MB +
+
+
+
+
+ +
+
+ Uptime + ${uptime} +
+
+
-
-
- Uptime - ${uptime} + +
+
+

Top Guilds

-
- Memory (Heap) - ${memoryUsage} MB -
-
- Node Version - ${process.version} -
-
- Platform - ${process.platform} +
+ ${topGuilds.map(g => ` +
+ ${g.name} + ${g.memberCount} members +
+ `).join('')}
@@ -89,9 +132,15 @@ export function dashboardRoute(): Response {

Quick Actions

- - - + + +
diff --git a/src/web/server.ts b/src/web/server.ts index 880bba4..87d4bcb 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -1,6 +1,7 @@ import { env } from "@/lib/env"; import { router } from "./router"; import type { Server } from "bun"; +import { AuroraClient } from "@/lib/BotClient"; export class WebServer { private static server: Server | null = null; @@ -42,16 +43,22 @@ export class WebServer { // Start a heartbeat loop this.heartbeatInterval = setInterval(() => { if (this.server) { - const uptime = process.uptime(); + const memoryUsage = process.memoryUsage(); this.server.publish("status-updates", JSON.stringify({ type: "HEARTBEAT", data: { - uptime, + guildCount: AuroraClient.guilds.cache.size, + userCount: AuroraClient.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0), + commandCount: AuroraClient.commands.size, + ping: AuroraClient.ws.ping, + memory: (memoryUsage.heapUsed / 1024 / 1024).toFixed(2), + memoryTotal: (memoryUsage.rss / 1024 / 1024).toFixed(2), + uptime: process.uptime(), timestamp: Date.now() } })); } - }, 5000); + }, 3000); // Update every 3 seconds for better responsiveness } public static stop() { diff --git a/src/web/views/layout.ts b/src/web/views/layout.ts index 3400ddb..703313d 100644 --- a/src/web/views/layout.ts +++ b/src/web/views/layout.ts @@ -24,14 +24,15 @@ export function BaseLayout({ title, content }: LayoutProps): string { - + +
-

Aurora Web

+

Aurora

@@ -39,12 +40,12 @@ export function BaseLayout({ title, content }: LayoutProps): string {