diff --git a/Dockerfile b/Dockerfile index 4a6e806..510ea55 100644 --- a/Dockerfile +++ b/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 diff --git a/docker-compose.yml b/docker-compose.yml index 6362023..0b6754c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/package.json b/package.json index f8c5c47..e150ca5 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/scripts/remote-dashboard.sh b/scripts/remote-dashboard.sh new file mode 100755 index 0000000..0cc6ad2 --- /dev/null +++ b/scripts/remote-dashboard.sh @@ -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 diff --git a/scripts/remote.sh b/scripts/remote.sh new file mode 100755 index 0000000..0081b73 --- /dev/null +++ b/scripts/remote.sh @@ -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 diff --git a/src/index.ts b/src/index.ts index 9ceae64..758aaee 100644 --- a/src/index.ts +++ b/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 => { + 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(); }; diff --git a/src/web/src/index.ts b/src/web/src/index.ts index b3dd7ab..021ddae 100644 --- a/src/web/src/index.ts +++ b/src/web/src/index.ts @@ -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}`); \ No newline at end of file diff --git a/src/web/src/server.ts b/src/web/src/server.ts new file mode 100644 index 0000000..0b33058 --- /dev/null +++ b/src/web/src/server.ts @@ -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; + stop: () => Promise; + 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 { + 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 { + 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); + } +} diff --git a/tickets/2026-01-07-replace-mock-dashboard-data.md b/tickets/2026-01-07-replace-mock-dashboard-data.md deleted file mode 100644 index 03a3058..0000000 --- a/tickets/2026-01-07-replace-mock-dashboard-data.md +++ /dev/null @@ -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.