feat: improvements to web dashboard

This commit is contained in:
syntaxbullet
2026-01-08 13:56:25 +01:00
parent 47507dd65a
commit 9e7f18787b
9 changed files with 332 additions and 108 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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
View 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

View File

@@ -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();
};

View File

@@ -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
View 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);
}
}

View File

@@ -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.