Files
aurorabot/web/src/server.ts
syntaxbullet 36f9c76fa9 refactor(web): convert server to API-only mode
- Remove build process spawning for frontend bundler
- Remove SPA fallback and static file serving
- Return 404 for unknown routes instead of serving index.html
- Keep all REST API endpoints and WebSocket functionality
2026-02-08 16:41:47 +01:00

901 lines
40 KiB
TypeScript

/**
* 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<typeof serve>;
stop: () => Promise<void>;
url: string;
}
/**
* Creates and starts the API server.
*/
export async function createWebServer(config: WebServerConfig = {}): Promise<WebServerInstance> {
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<import("@shared/modules/dashboard/dashboard.types").ActivityData[]> | 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<string, any>;
// 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<string, string> = {
"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 = <T>(result: PromiseSettledResult<T>, 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<WebServerInstance> {
// Current implementation doesn't need CWD switching thanks to absolute path resolution
return createWebServer(config);
}