forked from syntaxbullet/AuroraBot-discord
feat: improvements to web dashboard
This commit is contained in:
10
Dockerfile
10
Dockerfile
@@ -2,16 +2,20 @@ FROM oven/bun:latest AS base
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y git
|
||||
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dependencies
|
||||
# Install root project dependencies
|
||||
COPY package.json bun.lock ./
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# Install web project dependencies
|
||||
COPY src/web/package.json src/web/bun.lock ./src/web/
|
||||
RUN cd src/web && bun install --frozen-lockfile
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
# Expose ports (3000 for web dashboard)
|
||||
EXPOSE 3000
|
||||
|
||||
# Default command
|
||||
|
||||
@@ -6,11 +6,20 @@ services:
|
||||
- POSTGRES_USER=${DB_USER}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_DB=${DB_NAME}
|
||||
ports:
|
||||
- "127.0.0.1:${DB_PORT}:5432"
|
||||
# Uncomment to access DB from host (for debugging/drizzle-kit studio)
|
||||
# ports:
|
||||
# - "127.0.0.1:${DB_PORT}:5432"
|
||||
volumes:
|
||||
- ./src/db/data:/var/lib/postgresql/data
|
||||
- ./src/db/log:/var/log/postgresql
|
||||
networks:
|
||||
- internal
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}" ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
app:
|
||||
container_name: aurora_app
|
||||
restart: unless-stopped
|
||||
@@ -24,19 +33,30 @@ services:
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
- /app/src/web/node_modules
|
||||
environment:
|
||||
- HOST=0.0.0.0
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_NAME=${DB_NAME}
|
||||
- DB_PORT=${DB_PORT}
|
||||
- DB_PORT=5432
|
||||
- DB_HOST=db
|
||||
- DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
|
||||
- DISCORD_GUILD_ID=${DISCORD_GUILD_ID}
|
||||
- DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}
|
||||
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- internal
|
||||
- web
|
||||
healthcheck:
|
||||
test: [ "CMD", "bun", "-e", "fetch('http://localhost:3000/api/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))" ]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
command: bun run dev
|
||||
|
||||
studio:
|
||||
@@ -51,13 +71,24 @@ services:
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
- /app/src/web/node_modules
|
||||
environment:
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_NAME=${DB_NAME}
|
||||
- DB_PORT=${DB_PORT}
|
||||
- DB_PORT=5432
|
||||
- DB_HOST=db
|
||||
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- internal
|
||||
command: bun run db:studio
|
||||
|
||||
networks:
|
||||
internal:
|
||||
driver: bridge
|
||||
internal: true # No external access
|
||||
web:
|
||||
driver: bridge # Can be accessed from host
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
"dev": "bun --watch src/index.ts",
|
||||
"db:studio": "drizzle-kit studio --host 0.0.0.0",
|
||||
"studio:remote": "bash scripts/remote-studio.sh",
|
||||
"dashboard:remote": "bash scripts/remote-dashboard.sh",
|
||||
"remote": "bash scripts/remote.sh",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
43
scripts/remote-dashboard.sh
Executable file
43
scripts/remote-dashboard.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Load environment variables
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
source .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
if [ -z "$VPS_HOST" ] || [ -z "$VPS_USER" ]; then
|
||||
echo "Error: VPS_HOST and VPS_USER must be set in .env"
|
||||
echo "Please add them to your .env file:"
|
||||
echo "VPS_USER=your-username"
|
||||
echo "VPS_HOST=your-ip-address"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DASHBOARD_PORT=${DASHBOARD_PORT:-3000}
|
||||
|
||||
echo "🌐 Establishing secure tunnel to Aurora Dashboard..."
|
||||
echo "📊 Dashboard will be accessible at: http://localhost:$DASHBOARD_PORT"
|
||||
echo "Press Ctrl+C to stop the connection."
|
||||
echo ""
|
||||
|
||||
# Function to open browser (cross-platform)
|
||||
open_browser() {
|
||||
sleep 2
|
||||
if command -v open &> /dev/null; then
|
||||
open "http://localhost:$DASHBOARD_PORT"
|
||||
elif command -v xdg-open &> /dev/null; then
|
||||
xdg-open "http://localhost:$DASHBOARD_PORT"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if autossh is available
|
||||
if command -v autossh &> /dev/null; then
|
||||
SSH_CMD="autossh -M 0 -o ServerAliveInterval=30 -o ServerAliveCountMax=3"
|
||||
else
|
||||
SSH_CMD="ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=3"
|
||||
fi
|
||||
|
||||
open_browser &
|
||||
$SSH_CMD -N -L $DASHBOARD_PORT:127.0.0.1:$DASHBOARD_PORT $VPS_USER@$VPS_HOST
|
||||
61
scripts/remote.sh
Executable file
61
scripts/remote.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Load environment variables
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
source .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
if [ -z "$VPS_HOST" ] || [ -z "$VPS_USER" ]; then
|
||||
echo "Error: VPS_HOST and VPS_USER must be set in .env"
|
||||
echo "Please add them to your .env file:"
|
||||
echo "VPS_USER=your-username"
|
||||
echo "VPS_HOST=your-ip-address"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DASHBOARD_PORT=${DASHBOARD_PORT:-3000}
|
||||
STUDIO_PORT=${STUDIO_PORT:-4983}
|
||||
|
||||
echo "🚀 Establishing secure tunnels to Aurora services..."
|
||||
echo ""
|
||||
echo "📊 Dashboard: http://localhost:$DASHBOARD_PORT"
|
||||
echo "🔮 Studio: http://localhost:$STUDIO_PORT (https://local.drizzle.studio)"
|
||||
echo ""
|
||||
echo "Press Ctrl+C to stop all connections."
|
||||
echo ""
|
||||
|
||||
# Function to open browser (cross-platform)
|
||||
open_browser() {
|
||||
sleep 2 # Wait for tunnel to establish
|
||||
if command -v open &> /dev/null; then
|
||||
# macOS
|
||||
open "http://localhost:$DASHBOARD_PORT"
|
||||
elif command -v xdg-open &> /dev/null; then
|
||||
# Linux
|
||||
xdg-open "http://localhost:$DASHBOARD_PORT"
|
||||
elif command -v start &> /dev/null; then
|
||||
# Windows (Git Bash)
|
||||
start "http://localhost:$DASHBOARD_PORT"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if autossh is available for auto-reconnection
|
||||
if command -v autossh &> /dev/null; then
|
||||
echo "✅ Using autossh for automatic reconnection"
|
||||
SSH_CMD="autossh -M 0 -o ServerAliveInterval=30 -o ServerAliveCountMax=3"
|
||||
else
|
||||
echo "💡 Tip: Install autossh for automatic reconnection (brew install autossh)"
|
||||
SSH_CMD="ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=3"
|
||||
fi
|
||||
|
||||
# Open browser in background
|
||||
open_browser &
|
||||
|
||||
# Start both tunnels
|
||||
# -N means "Do not execute a remote command". -L is for local port forwarding.
|
||||
$SSH_CMD -N \
|
||||
-L $DASHBOARD_PORT:127.0.0.1:$DASHBOARD_PORT \
|
||||
-L $STUDIO_PORT:127.0.0.1:$STUDIO_PORT \
|
||||
$VPS_USER@$VPS_HOST
|
||||
68
src/index.ts
68
src/index.ts
@@ -1,15 +1,70 @@
|
||||
import { AuroraClient } from "@/lib/BotClient";
|
||||
import { env } from "@lib/env";
|
||||
|
||||
import { webServer } from "./web/src";
|
||||
import { join } from "node:path";
|
||||
|
||||
// Load commands & events
|
||||
await AuroraClient.loadCommands();
|
||||
await AuroraClient.loadEvents();
|
||||
await AuroraClient.deployCommands();
|
||||
|
||||
webServer.start();
|
||||
console.log("Web server is running on http://localhost:3000")
|
||||
console.log("🌐 Starting web server...");
|
||||
|
||||
const webProjectPath = join(import.meta.dir, "web");
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
let shuttingDown = false;
|
||||
|
||||
const startWebServer = () => {
|
||||
const args = isProduction
|
||||
? [process.execPath, "src/index.ts"]
|
||||
: [process.execPath, "--hot", "src/index.ts"];
|
||||
|
||||
return Bun.spawn(args, {
|
||||
cwd: webProjectPath,
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
WEB_PORT: process.env.WEB_PORT || "3000",
|
||||
...(process.env.HOST && { WEB_HOST: process.env.HOST }),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
let webServer = startWebServer();
|
||||
|
||||
// Monitor web server and restart on unexpected exit
|
||||
const monitorWebServer = async () => {
|
||||
const exitCode = await webServer.exited;
|
||||
if (!shuttingDown && exitCode !== 0) {
|
||||
console.warn(`⚠️ Web server exited with code ${exitCode}, restarting in 1s...`);
|
||||
await Bun.sleep(1000);
|
||||
webServer = startWebServer();
|
||||
monitorWebServer(); // Continue monitoring the new process
|
||||
}
|
||||
};
|
||||
monitorWebServer();
|
||||
|
||||
// Wait for web server to be ready
|
||||
const waitForWebServer = async (url: string, maxAttempts = 30): Promise<boolean> => {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
const res = await fetch(`${url}/api/health`);
|
||||
if (res.ok) return true;
|
||||
} catch {
|
||||
// Server not ready yet
|
||||
}
|
||||
await Bun.sleep(100);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const webPort = process.env.WEB_PORT || "3000";
|
||||
const webReady = await waitForWebServer(`http://localhost:${webPort}`);
|
||||
if (webReady) {
|
||||
console.log(`✅ Web server ready at http://localhost:${webPort}`);
|
||||
} else {
|
||||
console.warn("⚠️ Web server did not become ready in time, continuing anyway...");
|
||||
}
|
||||
|
||||
// login with the token from .env
|
||||
if (!env.DISCORD_BOT_TOKEN) {
|
||||
@@ -19,7 +74,10 @@ AuroraClient.login(env.DISCORD_BOT_TOKEN);
|
||||
|
||||
// Handle graceful shutdown
|
||||
const shutdownHandler = () => {
|
||||
webServer.stop();
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
console.log("🛑 Shutdown signal received. Stopping web server...");
|
||||
webServer.kill();
|
||||
AuroraClient.shutdown();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,46 +1,18 @@
|
||||
import { serve } from "bun";
|
||||
import index from "./index.html";
|
||||
/**
|
||||
* Web server entry point.
|
||||
*
|
||||
* This file can be run directly for standalone development:
|
||||
* bun --hot src/index.ts
|
||||
*
|
||||
* Or the server can be started in-process by importing from ./server.ts
|
||||
*/
|
||||
|
||||
import { createWebServer } from "./server";
|
||||
|
||||
const server = serve({
|
||||
routes: {
|
||||
// Serve index.html for all unmatched routes.
|
||||
"/*": index,
|
||||
|
||||
"/api/hello": {
|
||||
async GET(req) {
|
||||
return Response.json({
|
||||
message: "Hello, world!",
|
||||
method: "GET",
|
||||
});
|
||||
},
|
||||
async PUT(req) {
|
||||
return Response.json({
|
||||
message: "Hello, world!",
|
||||
method: "PUT",
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
"/api/hello/:name": async req => {
|
||||
const name = req.params.name;
|
||||
return Response.json({
|
||||
message: `Hello, ${name}!`,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
development: process.env.NODE_ENV !== "production" && {
|
||||
// Enable browser hot reloading in development
|
||||
hmr: true,
|
||||
|
||||
// Echo console logs from the browser to the server
|
||||
console: true,
|
||||
},
|
||||
|
||||
|
||||
|
||||
// Auto-start when run directly
|
||||
const instance = await createWebServer({
|
||||
port: Number(process.env.WEB_PORT) || 3000,
|
||||
hostname: process.env.WEB_HOST || "localhost",
|
||||
});
|
||||
|
||||
export const webServer = { start: () => server, stop: () => server.stop() };
|
||||
|
||||
console.log(`🌐 Web server is running at ${instance.url}`);
|
||||
105
src/web/src/server.ts
Normal file
105
src/web/src/server.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Web server factory module.
|
||||
* Exports a function to create and start the web server.
|
||||
* This allows the server to be started in-process from the main application.
|
||||
*/
|
||||
|
||||
import { serve } from "bun";
|
||||
|
||||
export interface WebServerConfig {
|
||||
port?: number;
|
||||
hostname?: string;
|
||||
}
|
||||
|
||||
export interface WebServerInstance {
|
||||
server: ReturnType<typeof serve>;
|
||||
stop: () => Promise<void>;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and starts the web server.
|
||||
*
|
||||
* IMPORTANT: This function must be called from within the web project directory
|
||||
* or the bundler won't resolve paths correctly. Use `startWebServerFromRoot`
|
||||
* if calling from the main application.
|
||||
*/
|
||||
export async function createWebServer(config: WebServerConfig = {}): Promise<WebServerInstance> {
|
||||
const { port = 3000, hostname = "localhost" } = config;
|
||||
|
||||
// Dynamic import of the HTML to ensure bundler context is correct
|
||||
const index = await import("./index.html");
|
||||
|
||||
const server = serve({
|
||||
port,
|
||||
hostname,
|
||||
routes: {
|
||||
// Serve index.html for all unmatched routes (SPA catch-all)
|
||||
"/*": index.default,
|
||||
|
||||
"/api/hello": {
|
||||
async GET(req) {
|
||||
return Response.json({
|
||||
message: "Hello, world!",
|
||||
method: "GET",
|
||||
});
|
||||
},
|
||||
async PUT(req) {
|
||||
return Response.json({
|
||||
message: "Hello, world!",
|
||||
method: "PUT",
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
"/api/hello/:name": async (req) => {
|
||||
const name = req.params.name;
|
||||
return Response.json({
|
||||
message: `Hello, ${name}!`,
|
||||
});
|
||||
},
|
||||
|
||||
"/api/health": () => Response.json({ status: "ok", timestamp: Date.now() }),
|
||||
},
|
||||
|
||||
development: process.env.NODE_ENV !== "production" && {
|
||||
hmr: true,
|
||||
console: true,
|
||||
},
|
||||
});
|
||||
|
||||
const url = `http://${hostname}:${port}`;
|
||||
|
||||
return {
|
||||
server,
|
||||
url,
|
||||
stop: async () => {
|
||||
server.stop(true);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the web server from the main application root.
|
||||
* Handles the working directory context switch needed for bundler resolution.
|
||||
*/
|
||||
export async function startWebServerFromRoot(
|
||||
webProjectPath: string,
|
||||
config: WebServerConfig = {}
|
||||
): Promise<WebServerInstance> {
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
try {
|
||||
// Change to web project directory for correct bundler resolution
|
||||
process.chdir(webProjectPath);
|
||||
|
||||
const instance = await createWebServer(config);
|
||||
|
||||
console.log(`🌐 Web server running at ${instance.url}`);
|
||||
|
||||
return instance;
|
||||
} finally {
|
||||
// Restore original working directory
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
|
||||
# 2026-01-07-replace-mock-dashboard-data.md: Replace Mock Dashboard Data with Live Telemetry
|
||||
|
||||
**Status:** Done
|
||||
**Created:** 2026-01-07
|
||||
**Tags:** dashboard, telemetry, logging, database
|
||||
|
||||
## 1. Context & User Story
|
||||
* **As a:** Bot Administrator
|
||||
* **I want to:** see actual system logs, real-time resource usage, and accurate database statistics on the web dashboard
|
||||
* **So that:** I can monitor the true health and activity of the Aurora application without checking the terminal or database manually.
|
||||
|
||||
## 2. Technical Requirements
|
||||
### Data Model Changes
|
||||
- [ ] No strict database schema changes required, but may need a cohesive `LogService` or in-memory buffer to store recent "Activity" events for the dashboard history.
|
||||
|
||||
### API / Interface
|
||||
- **Dashboard Route (`src/web/routes/dashboard.ts`):**
|
||||
- [x] Replace `mockedActivity` array with a fetch from a real log buffer/source.
|
||||
- [x] Replace `userCount` approximation with a precise count from `UserService` or `AuroraClient`.
|
||||
- [x] Replace "System Metrics" mock bars with real values (RAM usage, Uptime, CPU load if possible).
|
||||
- **Log Source:**
|
||||
- [x] Implement a mechanism (e.g., specific `Logger` transport or `WebServer` static buffer) to capture the last ~50 distinct application events (commands, errors, warnings) for display.
|
||||
- [ ] (Optional) If "Docker Compose Logs" are strictly required, implement a file reader for the standard output log file if accessible, otherwise rely on internal application logging.
|
||||
|
||||
### Real Data Integration
|
||||
- **Activity Feed:** Must show actual commands executed, system errors, and startup events.
|
||||
- **Top Stats:** Ensure `Servers`, `Users`, `Commands`, and `Ping` come from the live `AuroraClient` instance.
|
||||
- **Metrics:** Display `process.memoryUsage().heapUsed` converted to MB. Display `process.uptime()`.
|
||||
|
||||
## 3. Constraints & Validations (CRITICAL)
|
||||
- **Performance:** Fetching logs or stats must not block the event loop. Avoid heavy DB queries on every dashboard refresh; cache stats if necessary (e.g., via `setInterval` in background).
|
||||
- **Security:** Do not expose sensitive data (tokens, raw SQL) in the activity feed.
|
||||
- **Fallbacks:** If data is unavailable (e.g., client not ready), show "Loading..." or a neutral placeholder, not fake data.
|
||||
|
||||
## 4. Acceptance Criteria
|
||||
1. [x] The "Activity Feed" on the dashboard displays real, recent events that occurred in the application (e.g., "Bot started", "Command /ping executed").
|
||||
2. [x] The "System Metrics" section displays a visual representation (or text) of **actual** memory usage and uptime.
|
||||
3. [x] The hardcoded `mockedActivity` array is removed from `dashboard.ts`.
|
||||
4. [x] Refreshing the dashboard page updates the metrics and feed with the latest data.
|
||||
|
||||
## 5. Implementation Plan
|
||||
- [x] Step 1: Create a simple in-memory `LogBuffer` in `src/lib/logger.ts` (or similar) to keep the last 50 logs.
|
||||
- [x] Step 2: Hook this buffer into the existing logging system (or add manual pushes in `command.handler.ts` etc).
|
||||
- [x] Step 3: Implement `getSystemMetrics()` helper to return formatted RAM/CPU data.
|
||||
- [x] Step 4: Update `src/web/routes/dashboard.ts` to import the log buffer and metrics helper.
|
||||
- [x] Step 5: Replace the HTML template variables with these real data sources.
|
||||
|
||||
## Implementation Notes
|
||||
- **Log Buffer**: Added a 50-item rolling buffer in `src/lib/logger.ts` exposing `getRecentLogs()`.
|
||||
- **Dashboard Update**: `src/web/routes/dashboard.ts` now uses `AuroraClient` stats and `process` metrics (Uptime, Memory) directly.
|
||||
- **Tests**: Added `src/lib/logger.test.ts` to verify buffer logic.
|
||||
Reference in New Issue
Block a user