diff --git a/src/lib/logger.ts b/src/lib/logger.ts index 56fdd2b..a673676 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -1,3 +1,5 @@ +import { WebServer } from "@/web/server"; + /** * Centralized logging utility with consistent formatting */ @@ -7,6 +9,7 @@ export const logger = { */ info: (message: string, ...args: any[]) => { console.log(`ℹ️ ${message}`, ...args); + try { WebServer.broadcastLog("info", message); } catch { } }, /** @@ -14,6 +17,7 @@ export const logger = { */ success: (message: string, ...args: any[]) => { console.log(`✅ ${message}`, ...args); + try { WebServer.broadcastLog("success", message); } catch { } }, /** @@ -21,6 +25,7 @@ export const logger = { */ warn: (message: string, ...args: any[]) => { console.warn(`⚠️ ${message}`, ...args); + try { WebServer.broadcastLog("warning", message); } catch { } }, /** @@ -28,6 +33,7 @@ export const logger = { */ error: (message: string, ...args: any[]) => { console.error(`❌ ${message}`, ...args); + try { WebServer.broadcastLog("error", message); } catch { } }, /** @@ -35,5 +41,6 @@ export const logger = { */ debug: (message: string, ...args: any[]) => { console.log(`🔍 ${message}`, ...args); + try { WebServer.broadcastLog("debug", message); } catch { } }, }; diff --git a/src/web/public/script.js b/src/web/public/script.js index 9247ae1..26d3872 100644 --- a/src/web/public/script.js +++ b/src/web/public/script.js @@ -56,12 +56,41 @@ document.addEventListener("DOMContentLoaded", () => { // We can optionally verify if client clock is drifting, but let's keep it simple. } else if (msg.type === "WELCOME") { console.log(msg.message); + } else if (msg.type === "LOG") { + appendToActivityFeed(msg.data); } } catch (e) { console.error("WS Parse Error", e); } }; + function appendToActivityFeed(log) { + const list = document.querySelector(".activity-feed"); + if (!list) return; + + const item = document.createElement("li"); + item.className = `activity-item ${log.type}`; + + const timeSpan = document.createElement("span"); + timeSpan.className = "time"; + timeSpan.textContent = log.timestamp; + + const messageSpan = document.createElement("span"); + messageSpan.className = "message"; + messageSpan.textContent = log.message; + + item.appendChild(timeSpan); + item.appendChild(messageSpan); + + // Prepend to top + list.insertBefore(item, list.firstChild); + + // Limit history + if (list.children.length > 50) { + list.removeChild(list.lastChild); + } + } + ws.onclose = () => { console.log("WS Disconnected"); if (statusIndicator) statusIndicator.classList.remove("online"); diff --git a/src/web/public/style.css b/src/web/public/style.css index df772f8..0d064da 100644 --- a/src/web/public/style.css +++ b/src/web/public/style.css @@ -455,4 +455,153 @@ header nav a:hover::after { padding-left: 1rem; padding-right: 1rem; } -} \ No newline at end of file +} +/* Dashboard Layout */ +.dashboard-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.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; +} + +.panel.control-panel { + grid-column: 1 / -1; +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + border-bottom: 1px solid var(--border-color); + padding-bottom: 0.75rem; +} + +.panel-header h2 { + font-size: 1.1rem; + margin: 0; +} + +/* Activity Feed */ +.activity-feed { + list-style: none; + padding: 0; + margin: 0; + max-height: 300px; + 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; +} + +.activity-item .time { + color: var(--text-muted); + font-family: monospace; +} + +.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%); } + +.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; +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + +/* Mock Chart */ +.mock-chart-container { + height: 200px; + display: flex; + align-items: flex-end; + gap: 4px; + padding-top: 1rem; + border-bottom: 1px solid var(--border-color); + margin-bottom: 0.5rem; +} + +.mock-chart-bar { + flex: 1; + background: var(--primary); + opacity: 0.5; + border-radius: 2px 2px 0 0; + transition: height 0.5s ease; +} + +.mock-chart-bar:hover { + opacity: 0.8; +} + +.metrics-legend { + font-size: 0.8rem; + color: var(--text-muted); + text-align: center; +} + +/* Responsive Dashboard */ +@media (max-width: 768px) { + .dashboard-grid { + grid-template-columns: 1fr 1fr; /* 2 columns on tablet/mobile */ + } + + .dashboard-main { + grid-template-columns: 1fr; /* Stack panels */ + } +} diff --git a/src/web/router.test.ts b/src/web/router.test.ts index 87b3d7b..9f46776 100644 --- a/src/web/router.test.ts +++ b/src/web/router.test.ts @@ -13,6 +13,13 @@ describe("Web Router", () => { expect(text).toContain('id="uptime-display"'); }); + it("should return dashboard page on /dashboard", async () => { + const req = new Request("http://localhost/dashboard"); + const res = await router(req); + expect(res.status).toBe(200); + expect(await res.text()).toContain("Live Activity"); + }); + it("should return health check on /health", async () => { const req = new Request("http://localhost/health"); const res = await router(req); diff --git a/src/web/router.ts b/src/web/router.ts index 806e8a9..82ed695 100644 --- a/src/web/router.ts +++ b/src/web/router.ts @@ -1,5 +1,6 @@ import { homeRoute } from "./routes/home"; import { healthRoute } from "./routes/health"; +import { dashboardRoute } from "./routes/dashboard"; import { file } from "bun"; import { join, resolve } from "path"; @@ -19,12 +20,6 @@ export async function router(request: Request): Promise { const relativePath = url.pathname.replace(/^\/public/, ""); // Resolve full path - // We use join with relativePath. If relativePath starts with /, join handles it correctly - // effectively treating it as a segment. - // However, to be extra safe with 'resolve', we ensure we are resolving from publicDir. - // simple join(publicDir, relativePath) is usually enough with 'bun'. - // But we use 'resolve' to handle .. segments correctly. - // We prepend '.' to relativePath to ensure it's treated as relative to publicDir logic const normalizedRelative = relativePath.startsWith("/") ? "." + relativePath : relativePath; const requestedPath = resolve(publicDir, normalizedRelative); @@ -35,8 +30,6 @@ export async function router(request: Request): Promise { return new Response(staticFile); } } else { - // If path traversal detected, return 403 or 404. - // 403 indicates we caught them. return new Response("Forbidden", { status: 403 }); } } @@ -47,6 +40,9 @@ export async function router(request: Request): Promise { if (url.pathname === "/health") { return healthRoute(); } + if (url.pathname === "/dashboard") { + return dashboardRoute(); + } } return new Response("Not Found", { status: 404 }); diff --git a/src/web/routes/dashboard.ts b/src/web/routes/dashboard.ts new file mode 100644 index 0000000..0b89714 --- /dev/null +++ b/src/web/routes/dashboard.ts @@ -0,0 +1,95 @@ +import { BaseLayout } from "../views/layout"; +import { AuroraClient } from "@/lib/BotClient"; + +export function dashboardRoute(): Response { + // Gather real data where possible, mock where not + const guildCount = AuroraClient.guilds.cache.size; + const userCount = AuroraClient.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0); // Approximation + const commandCount = AuroraClient.commands.size; + const ping = AuroraClient.ws.ping; + + // In a real app, these would be dynamic charts or lists + const mockedActivity = [ + { time: "10:42:01", type: "info", message: "User 'Syntax' ran /profile" }, + { time: "10:41:55", type: "success", message: "Task 'HourlyCleanup' completed" }, + { time: "10:40:12", type: "warning", message: "API Latency spike detected (150ms)" }, + { time: "10:39:00", type: "info", message: "Bot connected to Gateway" }, + ]; + + const content = ` +
+ +
+

Servers

+
${guildCount}
+
+
+

Users

+
${userCount}
+
+
+

Commands

+
${commandCount}
+
+
+

Ping

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

Live Activity

+ LIVE +
+
    + ${mockedActivity.map(log => ` +
  • + ${log.time} + ${log.message} +
  • + `).join('')} +
  • --:--:-- Waiting for events...
  • +
+
+ +
+
+

System Metrics

+
+
+
+
+
+
+
+
+
+
+
+ CPU Load (Mock) +
+
+
+ + +
+
+

Quick Actions

+
+
+ + + +
+
+
+ `; + + const html = BaseLayout({ title: "Dashboard", content }); + + return new Response(html, { + headers: { "Content-Type": "text/html" }, + }); +} diff --git a/src/web/server.ts b/src/web/server.ts index 7715769..880bba4 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -65,7 +65,21 @@ export class WebServer { this.server = null; } } + public static get port(): number | undefined { return this.server?.port; } + + public static broadcastLog(type: string, message: string) { + if (this.server) { + this.server.publish("status-updates", JSON.stringify({ + type: "LOG", + data: { + timestamp: new Date().toLocaleTimeString(), + type, + message + } + })); + } + } } diff --git a/src/web/views/layout.ts b/src/web/views/layout.ts index 5ab8431..3400ddb 100644 --- a/src/web/views/layout.ts +++ b/src/web/views/layout.ts @@ -31,6 +31,7 @@ export function BaseLayout({ title, content }: LayoutProps): string {

Aurora Web

diff --git a/tickets/2026-01-07-web-interface-feature-expansion.md b/tickets/2026-01-07-web-interface-feature-expansion.md new file mode 100644 index 0000000..5cfcb6c --- /dev/null +++ b/tickets/2026-01-07-web-interface-feature-expansion.md @@ -0,0 +1,50 @@ +# 2026-01-07-web-interface-feature-expansion + +**Status:** Draft +**Created:** 2026-01-07 +**Tags:** product-design, feature-request, ui + +## 1. Context & User Story +* **As a:** Bot Administrator +* **I want to:** have more useful features on the web dashboard +* **So that:** getting insights into the bot's performance and managing it becomes easier than using text commands. + +## 2. Technical Requirements +### Proposed Features +1. **Live Console / Activity Feed:** + * Stream abbreviated logs or important events (e.g., "User X joined", "Command Y executed"). +2. **Metrics Dashboard:** + * Visual charts for command usage (Top 5 commands). + * Memory usage and CPU load over time. + * API Latency gauge. +3. **Command Palette / Control Panel:** + * Buttons to clear cache, reload configuration, or restart specific services. +4. **Guild/User Browser:** + * Read-only list of top guilds or users by activity/economy balance. + +### Data Model Changes +- [ ] May require exposing existing Service data to the Web module. + +### API / Interface +- [ ] `GET /api/stats` or `WS` subscription for metrics. +- [ ] `GET /api/logs` (tail). + +## 3. Constraints & Validations (CRITICAL) +- **Security:** Modifying bot state (Control Panel) requires strict authentication/authorization (Future Ticket). For now, read-only/safe actions only. +- **Privacy:** Do not expose sensitive user PII in the web logs or dashboard without encryption/masking. + +## 4. Acceptance Criteria +1. [ ] A list of prioritized features is approved. +2. [ ] UI Mockups (code or image) for the "Dashboard" view. +3. [ ] Data sources for these features are identified in the codebase. + +## 5. Implementation Plan +- [x] **Phase 1:** Brainstorm & Mockup (This Ticket). +- [ ] **Phase 2:** Create individual implementation tickets for selected features (e.g., "Implement Metrics Graph"). + +## Implementation Notes +- Created `/dashboard` route with a code-based mockup. +- Implemented responsive CSS Grid layout for the dashboard. +- Integrated real `AuroraClient` data for Server Count, User Count, and Command Count. +- Added placeholder UI for "Live Activity" and "Metrics". +- Next steps: Connect WebSocket "HEARTBEAT" to the dashboard metrics and implement real logger streaming.