/** * API server factory module. * Exports a function to create and start the API server. * This allows the server to be started in-process from the main application. */ import { serve } from "bun"; import { join, resolve, dirname } from "path"; import { logger } from "@shared/lib/logger"; export interface WebServerConfig { port?: number; hostname?: string; } export interface WebServerInstance { server: ReturnType; stop: () => Promise; url: string; } /** * Creates and starts the API server. */ export async function createWebServer(config: WebServerConfig = {}): Promise { const { port = 3000, hostname = "localhost" } = config; // Resolve directories for asset serving const currentDir = dirname(new URL(import.meta.url).pathname); // Configuration constants const MAX_CONNECTIONS = 10; const MAX_PAYLOAD_BYTES = 16384; // 16KB const IDLE_TIMEOUT_SECONDS = 60; // Interval for broadcasting stats to all connected WS clients let statsBroadcastInterval: Timer | undefined; // Cache for activity stats (heavy aggregation) let activityPromise: Promise | null = null; let lastActivityFetch: number = 0; const ACTIVITY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes const server = serve({ port, hostname, async fetch(req, server) { const url = new URL(req.url); // Upgrade to WebSocket if (url.pathname === "/ws") { // Security Check: limit concurrent connections const currentConnections = server.pendingWebSockets; if (currentConnections >= MAX_CONNECTIONS) { logger.warn("web", `Connection rejected: limit reached (${currentConnections}/${MAX_CONNECTIONS})`); return new Response("Connection limit reached", { status: 429 }); } const success = server.upgrade(req); if (success) return undefined; return new Response("WebSocket upgrade failed", { status: 400 }); } // API routes if (url.pathname === "/api/health") { return Response.json({ status: "ok", timestamp: Date.now() }); } if (url.pathname === "/api/stats") { try { const stats = await getFullDashboardStats(); return Response.json(stats); } catch (error) { logger.error("web", "Error fetching dashboard stats", error); return Response.json( { error: "Failed to fetch dashboard statistics" }, { status: 500 } ); } } if (url.pathname === "/api/stats/activity") { try { const now = Date.now(); // If we have a valid cache, return it if (activityPromise && (now - lastActivityFetch < ACTIVITY_CACHE_TTL)) { const data = await activityPromise; return Response.json(data); } // Otherwise, trigger a new fetch (deduplicated by the promise) if (!activityPromise || (now - lastActivityFetch >= ACTIVITY_CACHE_TTL)) { activityPromise = (async () => { const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service"); return await dashboardService.getActivityAggregation(); })(); lastActivityFetch = now; } const activity = await activityPromise; return Response.json(activity); } catch (error) { logger.error("web", "Error fetching activity stats", error); return Response.json( { error: "Failed to fetch activity statistics" }, { status: 500 } ); } } // Administrative Actions if (url.pathname.startsWith("/api/actions/") && req.method === "POST") { try { const { actionService } = await import("@shared/modules/admin/action.service"); const { MaintenanceModeSchema } = await import("@shared/modules/dashboard/dashboard.types"); if (url.pathname === "/api/actions/reload-commands") { const result = await actionService.reloadCommands(); return Response.json(result); } if (url.pathname === "/api/actions/clear-cache") { const result = await actionService.clearCache(); return Response.json(result); } if (url.pathname === "/api/actions/maintenance-mode") { const rawBody = await req.json(); const parsed = MaintenanceModeSchema.safeParse(rawBody); if (!parsed.success) { return Response.json({ error: "Invalid payload", issues: parsed.error.issues }, { status: 400 }); } const result = await actionService.toggleMaintenanceMode(parsed.data.enabled, parsed.data.reason); return Response.json(result); } } catch (error) { logger.error("web", "Error executing administrative action", error); return Response.json( { error: "Failed to execute administrative action" }, { status: 500 } ); } } // Quest Management if (url.pathname === "/api/quests" && req.method === "POST") { try { const { questService } = await import("@shared/modules/quest/quest.service"); const data = await req.json(); // Basic validation could be added here or rely on service/DB const result = await questService.createQuest({ name: data.name, description: data.description || "", triggerEvent: data.triggerEvent, requirements: { target: Number(data.target) || 1 }, rewards: { xp: Number(data.xpReward) || 0, balance: Number(data.balanceReward) || 0 } }); return Response.json({ success: true, quest: result[0] }); } catch (error) { logger.error("web", "Error creating quest", error); return Response.json( { error: "Failed to create quest", details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } if (url.pathname === "/api/quests" && req.method === "GET") { try { const { questService } = await import("@shared/modules/quest/quest.service"); const quests = await questService.getAllQuests(); return Response.json({ success: true, data: quests.map(q => ({ id: q.id, name: q.name, description: q.description, triggerEvent: q.triggerEvent, requirements: q.requirements, rewards: q.rewards, })), }); } catch (error) { logger.error("web", "Error fetching quests", error); return Response.json( { error: "Failed to fetch quests", details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } if (url.pathname.startsWith("/api/quests/") && req.method === "DELETE") { const id = parseInt(url.pathname.split("/").pop() || "0", 10); if (!id) { return Response.json({ error: "Invalid quest ID" }, { status: 400 }); } try { const { questService } = await import("@shared/modules/quest/quest.service"); const result = await questService.deleteQuest(id); if (result.length === 0) { return Response.json({ error: "Quest not found" }, { status: 404 }); } return Response.json({ success: true, deleted: result[0].id }); } catch (error) { logger.error("web", "Error deleting quest", error); return Response.json( { error: "Failed to delete quest", details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } if (url.pathname.startsWith("/api/quests/") && req.method === "PUT") { const id = parseInt(url.pathname.split("/").pop() || "0", 10); if (!id) { return Response.json({ error: "Invalid quest ID" }, { status: 400 }); } try { const { questService } = await import("@shared/modules/quest/quest.service"); const data = await req.json(); const result = await questService.updateQuest(id, { name: data.name, description: data.description, triggerEvent: data.triggerEvent, requirements: { target: Number(data.target) || 1 }, rewards: { xp: Number(data.xpReward) || 0, balance: Number(data.balanceReward) || 0 } }); if (result.length === 0) { return Response.json({ error: "Quest not found" }, { status: 404 }); } return Response.json({ success: true, quest: result[0] }); } catch (error) { logger.error("web", "Error updating quest", error); return Response.json( { error: "Failed to update quest", details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } // Settings Management if (url.pathname === "/api/settings") { try { if (req.method === "GET") { const { config } = await import("@shared/lib/config"); const { jsonReplacer } = await import("@shared/lib/utils"); return new Response(JSON.stringify(config, jsonReplacer), { headers: { "Content-Type": "application/json" } }); } if (req.method === "POST") { const partialConfig = await req.json(); const { saveConfig, config: currentConfig } = await import("@shared/lib/config"); const { deepMerge } = await import("@shared/lib/utils"); // Merge partial update into current config const mergedConfig = deepMerge(currentConfig, partialConfig); // saveConfig throws if validation fails saveConfig(mergedConfig); const { systemEvents, EVENTS } = await import("@shared/lib/events"); systemEvents.emit(EVENTS.ACTIONS.RELOAD_COMMANDS); return Response.json({ success: true }); } } catch (error) { logger.error("web", "Settings error", error); return Response.json( { error: "Failed to process settings request", details: error instanceof Error ? error.message : String(error) }, { status: 400 } ); } } if (url.pathname === "/api/settings/meta") { try { const { AuroraClient } = await import("../../bot/lib/BotClient"); const { env } = await import("@shared/lib/env"); if (!env.DISCORD_GUILD_ID) { return Response.json({ roles: [], channels: [] }); } const guild = AuroraClient.guilds.cache.get(env.DISCORD_GUILD_ID); if (!guild) { return Response.json({ roles: [], channels: [] }); } // Map roles and channels to a simplified format const roles = guild.roles.cache .sort((a, b) => b.position - a.position) .map(r => ({ id: r.id, name: r.name, color: r.hexColor })); const channels = guild.channels.cache .map(c => ({ id: c.id, name: c.name, type: c.type })); const commands = Array.from(AuroraClient.knownCommands.entries()) .map(([name, category]) => ({ name, category })) .sort((a, b) => { if (a.category !== b.category) return a.category.localeCompare(b.category); return a.name.localeCompare(b.name); }); return Response.json({ roles, channels, commands }); } catch (error) { logger.error("web", "Error fetching settings meta", error); return Response.json( { error: "Failed to fetch metadata" }, { status: 500 } ); } } // ===================================== // Items Management API // ===================================== // GET /api/items - List all items with filtering if (url.pathname === "/api/items" && req.method === "GET") { try { const { itemsService } = await import("@shared/modules/items/items.service"); const filters = { search: url.searchParams.get("search") || undefined, type: url.searchParams.get("type") || undefined, rarity: url.searchParams.get("rarity") || undefined, limit: url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit")!) : 100, offset: url.searchParams.get("offset") ? parseInt(url.searchParams.get("offset")!) : 0, }; const result = await itemsService.getAllItems(filters); const { jsonReplacer } = await import("@shared/lib/utils"); return new Response(JSON.stringify(result, jsonReplacer), { headers: { "Content-Type": "application/json" } }); } catch (error) { logger.error("web", "Error fetching items", error); return Response.json( { error: "Failed to fetch items", details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } // POST /api/items - Create new item (JSON or multipart with image) if (url.pathname === "/api/items" && req.method === "POST") { try { const { itemsService } = await import("@shared/modules/items/items.service"); const contentType = req.headers.get("content-type") || ""; let itemData: any; let imageFile: File | null = null; if (contentType.includes("multipart/form-data")) { // Handle multipart form with optional image const formData = await req.formData(); const jsonData = formData.get("data"); imageFile = formData.get("image") as File | null; if (typeof jsonData === "string") { itemData = JSON.parse(jsonData); } else { return Response.json({ error: "Missing item data" }, { status: 400 }); } } else { // JSON-only request itemData = await req.json(); } // Validate required fields if (!itemData.name || !itemData.type) { return Response.json( { error: "Missing required fields: name and type are required" }, { status: 400 } ); } // Check for duplicate name if (await itemsService.isNameTaken(itemData.name)) { return Response.json( { error: "An item with this name already exists" }, { status: 409 } ); } // Set placeholder URLs if image will be uploaded const placeholderUrl = "/assets/items/placeholder.png"; const createData = { name: itemData.name, description: itemData.description || null, rarity: itemData.rarity || "Common", type: itemData.type, price: itemData.price ? BigInt(itemData.price) : null, iconUrl: itemData.iconUrl || placeholderUrl, imageUrl: itemData.imageUrl || placeholderUrl, usageData: itemData.usageData || null, }; // Create the item const item = await itemsService.createItem(createData); // If image was provided, save it and update the item if (imageFile && item) { const assetsDir = resolve(currentDir, "../../bot/assets/graphics/items"); const fileName = `${item.id}.png`; const filePath = join(assetsDir, fileName); // Validate file type (check magic bytes for PNG/JPEG/WebP/GIF) const buffer = await imageFile.arrayBuffer(); const bytes = new Uint8Array(buffer); const isPNG = bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47; const isJPEG = bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF; const isWebP = bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50; const isGIF = bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46; if (!isPNG && !isJPEG && !isWebP && !isGIF) { // Rollback: delete the created item await itemsService.deleteItem(item.id); return Response.json( { error: "Invalid image file. Only PNG, JPEG, WebP, and GIF are allowed." }, { status: 400 } ); } // Check file size (max 15MB) if (buffer.byteLength > 15 * 1024 * 1024) { await itemsService.deleteItem(item.id); return Response.json( { error: "Image file too large. Maximum size is 15MB." }, { status: 400 } ); } // Save the file await Bun.write(filePath, buffer); // Update item with actual asset URL const assetUrl = `/assets/items/${fileName}`; await itemsService.updateItem(item.id, { iconUrl: assetUrl, imageUrl: assetUrl, }); // Return item with updated URLs const updatedItem = await itemsService.getItemById(item.id); const { jsonReplacer } = await import("@shared/lib/utils"); return new Response(JSON.stringify({ success: true, item: updatedItem }, jsonReplacer), { status: 201, headers: { "Content-Type": "application/json" } }); } const { jsonReplacer } = await import("@shared/lib/utils"); return new Response(JSON.stringify({ success: true, item }, jsonReplacer), { status: 201, headers: { "Content-Type": "application/json" } }); } catch (error) { logger.error("web", "Error creating item", error); return Response.json( { error: "Failed to create item", details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } // GET /api/items/:id - Get single item if (url.pathname.match(/^\/api\/items\/\d+$/) && req.method === "GET") { const id = parseInt(url.pathname.split("/").pop()!); try { const { itemsService } = await import("@shared/modules/items/items.service"); const item = await itemsService.getItemById(id); if (!item) { return Response.json({ error: "Item not found" }, { status: 404 }); } const { jsonReplacer } = await import("@shared/lib/utils"); return new Response(JSON.stringify(item, jsonReplacer), { headers: { "Content-Type": "application/json" } }); } catch (error) { logger.error("web", "Error fetching item", error); return Response.json( { error: "Failed to fetch item", details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } // PUT /api/items/:id - Update item if (url.pathname.match(/^\/api\/items\/\d+$/) && req.method === "PUT") { const id = parseInt(url.pathname.split("/").pop()!); try { const { itemsService } = await import("@shared/modules/items/items.service"); const data = await req.json() as Record; // Check if item exists const existing = await itemsService.getItemById(id); if (!existing) { return Response.json({ error: "Item not found" }, { status: 404 }); } // Check for duplicate name (if name is being changed) if (data.name && data.name !== existing.name) { if (await itemsService.isNameTaken(data.name, id)) { return Response.json( { error: "An item with this name already exists" }, { status: 409 } ); } } // Build update data const updateData: any = {}; if (data.name !== undefined) updateData.name = data.name; if (data.description !== undefined) updateData.description = data.description; if (data.rarity !== undefined) updateData.rarity = data.rarity; if (data.type !== undefined) updateData.type = data.type; if (data.price !== undefined) updateData.price = data.price ? BigInt(data.price) : null; if (data.iconUrl !== undefined) updateData.iconUrl = data.iconUrl; if (data.imageUrl !== undefined) updateData.imageUrl = data.imageUrl; if (data.usageData !== undefined) updateData.usageData = data.usageData; const updatedItem = await itemsService.updateItem(id, updateData); const { jsonReplacer } = await import("@shared/lib/utils"); return new Response(JSON.stringify({ success: true, item: updatedItem }, jsonReplacer), { headers: { "Content-Type": "application/json" } }); } catch (error) { logger.error("web", "Error updating item", error); return Response.json( { error: "Failed to update item", details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } // DELETE /api/items/:id - Delete item if (url.pathname.match(/^\/api\/items\/\d+$/) && req.method === "DELETE") { const id = parseInt(url.pathname.split("/").pop()!); try { const { itemsService } = await import("@shared/modules/items/items.service"); const existing = await itemsService.getItemById(id); if (!existing) { return Response.json({ error: "Item not found" }, { status: 404 }); } // Delete the item await itemsService.deleteItem(id); // Try to delete associated asset file const assetsDir = resolve(currentDir, "../../bot/assets/graphics/items"); const assetPath = join(assetsDir, `${id}.png`); try { const assetFile = Bun.file(assetPath); if (await assetFile.exists()) { await Bun.write(assetPath, ""); // Clear file // Note: Bun doesn't have a direct delete, but we can use unlink via node:fs const { unlink } = await import("node:fs/promises"); await unlink(assetPath); } } catch (e) { // Non-critical: log but don't fail logger.warn("web", `Could not delete asset file for item ${id}`, e); } return new Response(null, { status: 204 }); } catch (error) { logger.error("web", "Error deleting item", error); return Response.json( { error: "Failed to delete item", details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } // POST /api/items/:id/icon - Upload/update item icon if (url.pathname.match(/^\/api\/items\/\d+\/icon$/) && req.method === "POST") { const id = parseInt(url.pathname.split("/")[3] || "0"); try { const { itemsService } = await import("@shared/modules/items/items.service"); // Check if item exists const existing = await itemsService.getItemById(id); if (!existing) { return Response.json({ error: "Item not found" }, { status: 404 }); } // Parse multipart form const formData = await req.formData(); const imageFile = formData.get("image") as File | null; if (!imageFile) { return Response.json({ error: "No image file provided" }, { status: 400 }); } // Validate file type const buffer = await imageFile.arrayBuffer(); const bytes = new Uint8Array(buffer); const isPNG = bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47; const isJPEG = bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF; const isWebP = bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50; const isGIF = bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46; if (!isPNG && !isJPEG && !isWebP && !isGIF) { return Response.json( { error: "Invalid image file. Only PNG, JPEG, WebP, and GIF are allowed." }, { status: 400 } ); } // Check file size (max 15MB) if (buffer.byteLength > 15 * 1024 * 1024) { return Response.json( { error: "Image file too large. Maximum size is 15MB." }, { status: 400 } ); } // Save the file const assetsDir = resolve(currentDir, "../../bot/assets/graphics/items"); const fileName = `${id}.png`; const filePath = join(assetsDir, fileName); await Bun.write(filePath, buffer); // Update item with new icon URL const assetUrl = `/assets/items/${fileName}`; const updatedItem = await itemsService.updateItem(id, { iconUrl: assetUrl, imageUrl: assetUrl, }); const { jsonReplacer } = await import("@shared/lib/utils"); return new Response(JSON.stringify({ success: true, item: updatedItem }, jsonReplacer), { headers: { "Content-Type": "application/json" } }); } catch (error) { logger.error("web", "Error uploading item icon", error); return Response.json( { error: "Failed to upload icon", details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } // ===================================== // Static Asset Serving (/assets/*) // ===================================== if (url.pathname.startsWith("/assets/")) { const assetsRoot = resolve(currentDir, "../../bot/assets/graphics"); const assetPath = url.pathname.replace("/assets/", ""); // Security: prevent path traversal const safePath = join(assetsRoot, assetPath); if (!safePath.startsWith(assetsRoot)) { return new Response("Forbidden", { status: 403 }); } const file = Bun.file(safePath); if (await file.exists()) { // Determine MIME type based on extension const ext = safePath.split(".").pop()?.toLowerCase(); const mimeTypes: Record = { "png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", "webp": "image/webp", "gif": "image/gif", }; const contentType = mimeTypes[ext || ""] || "application/octet-stream"; return new Response(file, { headers: { "Content-Type": contentType, "Cache-Control": "public, max-age=86400", // Cache for 24 hours } }); } return new Response("Not found", { status: 404 }); } // No frontend - return 404 for unknown routes return new Response("Not Found", { status: 404 }); }, websocket: { open(ws) { ws.subscribe("dashboard"); logger.debug("web", `Client connected. Total: ${server.pendingWebSockets}`); // Send initial stats getFullDashboardStats().then(stats => { ws.send(JSON.stringify({ type: "STATS_UPDATE", data: stats })); }); // Start broadcast interval if this is the first client if (!statsBroadcastInterval) { statsBroadcastInterval = setInterval(async () => { try { const stats = await getFullDashboardStats(); server.publish("dashboard", JSON.stringify({ type: "STATS_UPDATE", data: stats })); } catch (error) { logger.error("web", "Error in stats broadcast", error); } }, 5000); } }, async message(ws, message) { try { const messageStr = message.toString(); // Defense-in-depth: redundant length check before parsing if (messageStr.length > MAX_PAYLOAD_BYTES) { logger.error("web", "Payload exceeded maximum limit"); return; } const rawData = JSON.parse(messageStr); const { WsMessageSchema } = await import("@shared/modules/dashboard/dashboard.types"); const parsed = WsMessageSchema.safeParse(rawData); if (!parsed.success) { logger.error("web", "Invalid message format", parsed.error.issues); return; } if (parsed.data.type === "PING") { ws.send(JSON.stringify({ type: "PONG" })); } } catch (e) { logger.error("web", "Failed to handle message", e); } }, close(ws) { ws.unsubscribe("dashboard"); logger.debug("web", `Client disconnected. Total remaining: ${server.pendingWebSockets}`); // Stop broadcast interval if no clients left if (server.pendingWebSockets === 0 && statsBroadcastInterval) { clearInterval(statsBroadcastInterval); statsBroadcastInterval = undefined; } }, maxPayloadLength: MAX_PAYLOAD_BYTES, idleTimeout: IDLE_TIMEOUT_SECONDS, }, }); /** * Helper to fetch full dashboard stats object. * Unified for both HTTP API and WebSocket broadcasts. */ async function getFullDashboardStats() { // Import services (dynamic to avoid circular deps) const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service"); const { lootdropService } = await import("@shared/modules/economy/lootdrop.service"); const { getClientStats } = await import("../../bot/lib/clientStats"); // Fetch all data in parallel with error isolation const results = await Promise.allSettled([ Promise.resolve(getClientStats()), dashboardService.getActiveUserCount(), dashboardService.getTotalUserCount(), dashboardService.getEconomyStats(), dashboardService.getRecentEvents(10), dashboardService.getTotalItems(), dashboardService.getActiveLootdrops(), dashboardService.getLeaderboards(), Promise.resolve(lootdropService.getLootdropState()), ]); // Helper to unwrap result or return default const unwrap = (result: PromiseSettledResult, defaultValue: T, name: string): T => { if (result.status === 'fulfilled') return result.value; logger.error("web", `Failed to fetch ${name}`, result.reason); return defaultValue; }; const clientStats = unwrap(results[0], { bot: { name: 'Aurora', avatarUrl: null, status: null }, guilds: 0, commandsRegistered: 0, commandsKnown: 0, cachedUsers: 0, ping: 0, uptime: 0, lastCommandTimestamp: null }, 'clientStats'); const activeUsers = unwrap(results[1], 0, 'activeUsers'); const totalUsers = unwrap(results[2], 0, 'totalUsers'); const economyStats = unwrap(results[3], { totalWealth: 0n, avgLevel: 0, topStreak: 0 }, 'economyStats'); const recentEvents = unwrap(results[4], [], 'recentEvents'); const totalItems = unwrap(results[5], 0, 'totalItems'); const activeLootdrops = unwrap(results[6], [], 'activeLootdrops'); const leaderboards = unwrap(results[7], { topLevels: [], topWealth: [], topNetWorth: [] }, 'leaderboards'); const lootdropState = unwrap(results[8], undefined, 'lootdropState'); return { bot: clientStats.bot, guilds: { count: clientStats.guilds }, users: { active: activeUsers, total: totalUsers }, commands: { total: clientStats.commandsKnown, active: clientStats.commandsRegistered, disabled: clientStats.commandsKnown - clientStats.commandsRegistered }, ping: { avg: clientStats.ping }, economy: { totalWealth: economyStats.totalWealth.toString(), avgLevel: economyStats.avgLevel, topStreak: economyStats.topStreak, totalItems, }, recentEvents: recentEvents.map(event => ({ ...event, timestamp: event.timestamp instanceof Date ? event.timestamp.toISOString() : event.timestamp, })), activeLootdrops: activeLootdrops.map(drop => ({ rewardAmount: drop.rewardAmount, currency: drop.currency, createdAt: drop.createdAt.toISOString(), expiresAt: drop.expiresAt ? drop.expiresAt.toISOString() : null, // Explicitly excluding channelId/messageId to prevent sniping })), lootdropState, leaderboards, uptime: clientStats.uptime, lastCommandTimestamp: clientStats.lastCommandTimestamp, maintenanceMode: (await import("../../bot/lib/BotClient")).AuroraClient.maintenanceMode, }; } // Listen for real-time events from the system bus const { systemEvents, EVENTS } = await import("@shared/lib/events"); systemEvents.on(EVENTS.DASHBOARD.NEW_EVENT, (event) => { server.publish("dashboard", JSON.stringify({ type: "NEW_EVENT", data: event })); }); const url = `http://${hostname}:${port}`; return { server, url, stop: async () => { if (statsBroadcastInterval) { clearInterval(statsBroadcastInterval); } server.stop(true); }, }; } /** * Starts the web server from the main application root. * Kept for backward compatibility, but assumes webProjectPath is handled internally or ignored * in favor of relative path resolution from this file. */ export async function startWebServerFromRoot( webProjectPath: string, config: WebServerConfig = {} ): Promise { // Current implementation doesn't need CWD switching thanks to absolute path resolution return createWebServer(config); }