diff --git a/bot/lib/clientStats.ts b/bot/lib/clientStats.ts index 39a2f9e..88ed681 100644 --- a/bot/lib/clientStats.ts +++ b/bot/lib/clientStats.ts @@ -20,6 +20,10 @@ export function getClientStats(): ClientStats { // Fetch fresh stats const stats: ClientStats = { + bot: { + name: AuroraClient.user?.username || "Aurora", + avatarUrl: AuroraClient.user?.displayAvatarURL() || null, + }, guilds: AuroraClient.guilds.cache.size, ping: AuroraClient.ws.ping, cachedUsers: AuroraClient.users.cache.size, diff --git a/shared/modules/dashboard/dashboard.service.test.ts b/shared/modules/dashboard/dashboard.service.test.ts index 4643063..e8a1b4d 100644 --- a/shared/modules/dashboard/dashboard.service.test.ts +++ b/shared/modules/dashboard/dashboard.service.test.ts @@ -37,6 +37,7 @@ describe("dashboardService", () => { describe("getActiveUserCount", () => { test("should return active user count from database", async () => { mockSelect.mockImplementationOnce(() => ({ + // @ts-ignore ts(2322) from: mock(() => ({ where: mock(() => Promise.resolve([{ count: "5" }])), })), @@ -48,7 +49,9 @@ describe("dashboardService", () => { }); test("should return 0 when no users found", async () => { + mockSelect.mockImplementationOnce(() => ({ + // @ts-ignore ts(2322) from: mock(() => ({ where: mock(() => Promise.resolve([{ count: "0" }])), })), diff --git a/shared/modules/dashboard/dashboard.types.ts b/shared/modules/dashboard/dashboard.types.ts index f5bb2df..da230c0 100644 --- a/shared/modules/dashboard/dashboard.types.ts +++ b/shared/modules/dashboard/dashboard.types.ts @@ -1,4 +1,8 @@ export interface DashboardStats { + bot: { + name: string; + avatarUrl: string | null; + }; guilds: { count: number; changeFromLastMonth?: number; @@ -32,6 +36,10 @@ export interface RecentEvent { } export interface ClientStats { + bot: { + name: string; + avatarUrl: string | null; + }; guilds: number; ping: number; cachedUsers: number; diff --git a/tickets/2026-01-08-dashboard-activity-charts.md b/tickets/2026-01-08-dashboard-activity-charts.md new file mode 100644 index 0000000..7b85b4a --- /dev/null +++ b/tickets/2026-01-08-dashboard-activity-charts.md @@ -0,0 +1,38 @@ +# DASH-003: Visual Analytics & Activity Charts + +**Status:** Draft +**Created:** 2026-01-08 +**Tags:** dashboard, analytics, charts, frontend + +## 1. Context & User Story +* **As a:** Bot Administrator +* **I want to:** View a graphical representation of bot usage over the last 24 hours. +* **So that:** I can identify peak usage times and trends in command execution. + +## 2. Technical Requirements +### Data Model Changes +- [ ] No new tables. +- [ ] Requires complex aggregation queries on the `transactions` table. + +### API / Interface +- [ ] `GET /api/stats/activity`: Returns an array of data points for the last 24 hours (hourly granularity). +- [ ] Response Structure: `Array<{ hour: string, commands: number, transactions: number }>`. + +## 3. Constraints & Validations (CRITICAL) +- **Input Validation:** Hourly buckets must be strictly validated for the 24h window. +- **System Constraints:** + - Database query must be cached for at least 5 minutes as it involves heavy aggregation. + - Chart must be responsive and handle mobile viewports. +- **Business Logic Guardrails:** + - If no data exists for an hour, it must return 0 rather than skipping the point. + +## 4. Acceptance Criteria +1. [ ] **Given** a 24-hour history of transactions, **When** the dashboard loads, **Then** a line or area chart displays the command volume over time. +2. [ ] **Given** the premium glassmorphic theme, **When** the chart is rendered, **Then** it must use the primary brand colors and gradients to match the UI. +3. [ ] **Given** a mouse hover on the chart, **When** hovering over a point, **Then** a glassmorphic tooltip shows exact counts for that hour. + +## 5. Implementation Plan +- [ ] Step 1: Add an aggregation method to `dashboard.service.ts` to fetch hourly counts from the `transactions` table. +- [ ] Step 2: Create the `/api/stats/activity` endpoint. +- [ ] Step 3: Install a charting library (e.g., `recharts` or `lucide-react` compatible library). +- [ ] Step 4: Implement the `ActivityChart` component into the middle column of the dashboard. diff --git a/tickets/2026-01-08-dashboard-control-panel.md b/tickets/2026-01-08-dashboard-control-panel.md new file mode 100644 index 0000000..05862a9 --- /dev/null +++ b/tickets/2026-01-08-dashboard-control-panel.md @@ -0,0 +1,38 @@ +# DASH-004: Administrative Control Panel + +**Status:** Draft +**Created:** 2026-01-08 +**Tags:** dashboard, control-panel, bot-actions, operations + +## 1. Context & User Story +* **As a:** Bot Administrator +* **I want to:** Execute common maintenance tasks directly from the dashboard buttons. +* **So that:** I don't have to use terminal commands or Discord slash commands for system-level operations. + +## 2. Technical Requirements +### Data Model Changes +- [ ] N/A. + +### API / Interface +- [ ] `POST /api/actions/reload-commands`: Triggers the bot's command loader. +- [ ] `POST /api/actions/clear-cache`: Clears internal bot caches. +- [ ] `POST /api/actions/maintenance-mode`: Toggles a maintenance flag for the bot. + +## 3. Constraints & Validations (CRITICAL) +- **Input Validation:** Standard JSON body with optional `reason` field. +- **System Constraints:** + - Actions must be idempotent where possible. + - Actions must provide a response within 10 seconds. +- **Business Logic Guardrails:** + - **SECURITY**: This endpoint MUST require high-privilege authentication (currently we have single admin assumption, but token-based check should be planned). + - Maintenance mode toggle must be logged to the event feed. + +## 4. Acceptance Criteria +1. [ ] **Given** a "Quick Actions" card, **When** the "Reload Commands" button is clicked, **Then** the bot reloads its local command files and posts a "Success" event to the feed. +2. [ ] **Given** a running bot, **When** the "Clear Cache" button is pushed, **Then** the bot flushes its internal memory maps and the memory usage metric reflects the drop. + +## 5. Implementation Plan +- [ ] Step 1: Create an `action.service.ts` to handle the logic of triggering bot-specific functions. +- [ ] Step 2: Implement the `/api/actions` route group. +- [ ] Step 3: Design a "Quick Actions" card with premium styled buttons in `Dashboard.tsx`. +- [ ] Step 4: Add loading states to buttons to show when an operation is "In Progress." diff --git a/tickets/2026-01-08-real-time-dashboard-updates.md b/tickets/2026-01-08-real-time-dashboard-updates.md new file mode 100644 index 0000000..f02486b --- /dev/null +++ b/tickets/2026-01-08-real-time-dashboard-updates.md @@ -0,0 +1,43 @@ +# DASH-002: Real-time Live Updates via WebSockets + +**Status:** Draft +**Created:** 2026-01-08 +**Tags:** dashboard, websocket, real-time, performance + +## 1. Context & User Story +* **As a:** Bot Administrator +* **I want to:** See metrics and events update instantly on my screen without refreshing or waiting for polling intervals. +* **So that:** I can react immediately to errors or spikes in latency and have a dashboard that feels "alive." + +## 2. Technical Requirements +### Data Model Changes +- [ ] No database schema changes required. +- [ ] Redis or an in-memory event emitter might be useful for broadcasting events. + +### API / Interface +- [ ] Establish a WebSocket endpoint at `/ws/stats`. +- [ ] Define the message protocol: + - `HEARTBEAT`: Client to server to keep connection alive. + - `STATS_UPDATE`: Server to client containing partial or full `DashboardStats`. + - `NEW_EVENT`: Server to client when a transaction or moderation case occurs. + +## 3. Constraints & Validations (CRITICAL) +- **Input Validation:** WS messages must be validated using Zod. +- **System Constraints:** + - Limit to 10 concurrent WS connections to prevent server strain. + - Maximum message size: 16KB. + - Connection timeout: 60s of inactivity triggers a disconnect. +- **Business Logic Guardrails:** + - Websocket updates should not exceed 1 update per second for metrics. + - Events are pushed immediately as they occur. + +## 4. Acceptance Criteria +1. [ ] **Given** the dashboard is open, **When** a command is run in Discord, **Then** the "Recent Events" list updates instantly on the web UI. +2. [ ] **Given** a changing network environment, **When** the bot's ping fluctuates, **Then** the "Avg Latency" card updates in real-time. +3. [ ] **Given** a connection loss, **When** the network returns, **Then** the client automatically reconnects to the WS room. + +## 5. Implementation Plan +- [ ] Step 1: Integrate a WebSocket library (e.g., `bun`'s native `ws` or `socket.io`) into `web/src/server.ts`. +- [ ] Step 2: Implement a broadcast system in `dashboard.service.ts` to push events to the WS handler. +- [ ] Step 3: Create a `useDashboardSocket` hook in the frontend to handle connection lifecycle. +- [ ] Step 4: Refactor `Dashboard.tsx` to prefer WebSocket data over periodic polling. diff --git a/web/build.ts b/web/build.ts index 33b97d5..d655e48 100644 --- a/web/build.ts +++ b/web/build.ts @@ -127,28 +127,46 @@ const entrypoints = [...new Bun.Glob("**.html").scanSync("src")] .filter(dir => !dir.includes("node_modules")); console.log(`šŸ“„ Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`); -const result = await Bun.build({ - entrypoints, - outdir, - plugins: [plugin], - minify: true, - target: "browser", - sourcemap: "linked", - define: { - "process.env.NODE_ENV": JSON.stringify("production"), - }, - ...cliConfig, -}); +const build = async () => { + const result = await Bun.build({ + entrypoints, + outdir, + plugins: [plugin], + minify: true, + target: "browser", + sourcemap: "linked", + define: { + "process.env.NODE_ENV": JSON.stringify("production"), + }, + ...cliConfig, + }); + + const outputTable = result.outputs.map(output => ({ + File: path.relative(process.cwd(), output.path), + Type: output.kind, + Size: formatFileSize(output.size), + })); + + console.table(outputTable); + return result; +}; + +const result = await build(); const end = performance.now(); - -const outputTable = result.outputs.map(output => ({ - File: path.relative(process.cwd(), output.path), - Type: output.kind, - Size: formatFileSize(output.size), -})); - -console.table(outputTable); const buildTime = (end - start).toFixed(2); - console.log(`\nāœ… Build completed in ${buildTime}ms\n`); + +if ((cliConfig as any).watch) { + console.log("šŸ‘€ Watching for changes...\n"); + // Keep the process alive for watch mode + // Bun.build with watch:true handles the watching, + // we just need to make sure the script doesn't exit. + process.stdin.resume(); + + // Also, handle manual exit + process.on("SIGINT", () => { + console.log("\nšŸ‘‹ Stopping build watcher..."); + process.exit(0); + }); +} diff --git a/web/src/components/AppSidebar.tsx b/web/src/components/AppSidebar.tsx index 3a08a68..98357bc 100644 --- a/web/src/components/AppSidebar.tsx +++ b/web/src/components/AppSidebar.tsx @@ -1,5 +1,4 @@ - -import { LayoutDashboard, Settings, Activity, Server, Zap } from "lucide-react"; +import { LayoutDashboard, Settings, Activity } from "lucide-react"; import { Link, useLocation } from "react-router-dom"; import { Sidebar, @@ -14,6 +13,7 @@ import { SidebarFooter, SidebarRail, } from "@/components/ui/sidebar"; +import { useDashboardStats } from "@/hooks/use-dashboard-stats"; // Menu items. const items = [ @@ -36,37 +36,52 @@ const items = [ export function AppSidebar() { const location = useLocation(); + const { stats } = useDashboardStats(); + + const botName = stats?.bot?.name || "Aurora"; + const botAvatar = stats?.bot?.avatarUrl; return ( - - + + - - -
- + + +
+ {botAvatar ? ( + {botName} + ) : ( +
A
+ )}
-
- Aurora - v1.0.0 +
+ {botName} + Admin Portal
- + - Application + Main Navigation - + {items.map((item) => ( - - - - {item.title} + + + + {item.title} @@ -75,16 +90,16 @@ export function AppSidebar() { - + - -
- U + +
+ A
-
- User - Admin +
+ Administrator + Session Active
diff --git a/web/src/hooks/use-dashboard-stats.ts b/web/src/hooks/use-dashboard-stats.ts index ad1dfd8..79a9f35 100644 --- a/web/src/hooks/use-dashboard-stats.ts +++ b/web/src/hooks/use-dashboard-stats.ts @@ -1,6 +1,10 @@ import { useState, useEffect } from "react"; interface DashboardStats { + bot: { + name: string; + avatarUrl: string | null; + }; guilds: { count: number; }; diff --git a/web/src/index.html b/web/src/index.html index 6f502de..bf18beb 100644 --- a/web/src/index.html +++ b/web/src/index.html @@ -4,9 +4,11 @@ - Aurora - - + Aurora Dashboard + + + +
diff --git a/web/src/layouts/DashboardLayout.tsx b/web/src/layouts/DashboardLayout.tsx index 5539c8a..4715281 100644 --- a/web/src/layouts/DashboardLayout.tsx +++ b/web/src/layouts/DashboardLayout.tsx @@ -7,21 +7,28 @@ import { Separator } from "../components/ui/separator"; export function DashboardLayout() { return ( +
+
+
+
+ - -
- - -
- {/* Breadcrumbs could go here */} -

Dashboard

+ +
+ + +
+

Dashboard

+
+
+
-
-
+
+
-
+ ); diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index b542900..743ecd5 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -50,119 +50,115 @@ export function Dashboard() { } return ( -
-
-

Dashboard

-

Overview of your bot's activity and performance.

+
+
+

+ {stats.bot.name} Overview +

+

Monitoring real-time activity and core bot metrics.

-
+
{/* Metric Cards */} - - - Total Servers - - - -
{stats.guilds.count}
-

Active guilds

-
-
- - - - Active Users - - - -
{stats.users.active.toLocaleString()}
-

- {stats.users.total.toLocaleString()} total registered -

-
-
- - - - Commands - - - -
{stats.commands.total}
-

Registered commands

-
-
- - - - Avg Ping - - - -
{stats.ping.avg}ms
-

WebSocket latency

-
-
+ {[ + { title: "Active Users", value: stats.users.active.toLocaleString(), label: `${stats.users.total.toLocaleString()} total registered`, icon: Users, color: "from-purple-500 to-pink-500" }, + { title: "Commands registered", value: stats.commands.total, label: "Total system capabilities", icon: Zap, color: "from-yellow-500 to-orange-500" }, + { title: "Avg Latency", value: `${stats.ping.avg}ms`, label: "WebSocket heartbeat", icon: Activity, color: "from-emerald-500 to-teal-500" }, + ].map((metric, i) => ( + + + {metric.title} +
+ +
+
+ +
{metric.value}
+

{metric.label}

+
+
+ ))}
-
- - - Economy Overview - Server economy statistics +
+ + +
+ +
+ Economy Overview + + Global wealth and progression statistics +
+
+
+ + Uptime: {Math.floor(stats.uptime / 3600)}h {Math.floor((stats.uptime % 3600) / 60)}m + +
-
-
-

Total Wealth

-

{BigInt(stats.economy.totalWealth).toLocaleString()} AU

-
-
-
-

Average Level

-

{stats.economy.avgLevel}

+
+
+
+
+

Total Distributed Wealth

+

+ {BigInt(stats.economy.totalWealth).toLocaleString()} AU +

-
-

Top Streak

-

{stats.economy.topStreak} days

+
+
+
+

Avg Level

+

{stats.economy.avgLevel}

+
+
+

Peak Streak

+

{stats.economy.topStreak} days

- - - Recent Events - Latest system and bot events. + + + Recent Events + Live system activity feed - -
+ +
{stats.recentEvents.length === 0 ? ( -

No recent events

+
+

No activity recorded

+
) : ( - stats.recentEvents.slice(0, 5).map((event, i) => ( -
-
+ stats.recentEvents.slice(0, 6).map((event, i) => ( +
+
+
{event.icon}
+
-

- {event.icon} {event.message} +

+ {event.message}

-

- {new Date(event.timestamp).toLocaleString()} +

+ {new Date(event.timestamp).toLocaleTimeString()}

)) )}
+ {stats.recentEvents.length > 0 && ( + + )}
diff --git a/web/src/server.ts b/web/src/server.ts index 4ac1838..9a6d625 100644 --- a/web/src/server.ts +++ b/web/src/server.ts @@ -78,6 +78,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise