Compare commits
3 Commits
f79ee6fbc7
...
c7730b9355
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7730b9355 | ||
|
|
1e20a5a7a0 | ||
|
|
54944283a3 |
@@ -1,5 +1,6 @@
|
|||||||
import { AutocompleteInteraction } from "discord.js";
|
import { AutocompleteInteraction } from "discord.js";
|
||||||
import { AuroraClient } from "@/lib/BotClient";
|
import { AuroraClient } from "@/lib/BotClient";
|
||||||
|
import { logger } from "@shared/lib/logger";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,7 +17,7 @@ export class AutocompleteHandler {
|
|||||||
try {
|
try {
|
||||||
await command.autocomplete(interaction);
|
await command.autocomplete(interaction);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error handling autocomplete for ${interaction.commandName}:`, error);
|
logger.error("bot", `Error handling autocomplete for ${interaction.commandName}`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ChatInputCommandInteraction, MessageFlags } from "discord.js";
|
|||||||
import { AuroraClient } from "@/lib/BotClient";
|
import { AuroraClient } from "@/lib/BotClient";
|
||||||
import { userService } from "@shared/modules/user/user.service";
|
import { userService } from "@shared/modules/user/user.service";
|
||||||
import { createErrorEmbed } from "@lib/embeds";
|
import { createErrorEmbed } from "@lib/embeds";
|
||||||
|
import { logger } from "@shared/lib/logger";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,7 +14,7 @@ export class CommandHandler {
|
|||||||
const command = AuroraClient.commands.get(interaction.commandName);
|
const command = AuroraClient.commands.get(interaction.commandName);
|
||||||
|
|
||||||
if (!command) {
|
if (!command) {
|
||||||
console.error(`No command matching ${interaction.commandName} was found.`);
|
logger.error("bot", `No command matching ${interaction.commandName} was found.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,14 +29,14 @@ export class CommandHandler {
|
|||||||
try {
|
try {
|
||||||
await userService.getOrCreateUser(interaction.user.id, interaction.user.username);
|
await userService.getOrCreateUser(interaction.user.id, interaction.user.username);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to ensure user exists:", error);
|
logger.error("bot", "Failed to ensure user exists", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await command.execute(interaction);
|
await command.execute(interaction);
|
||||||
AuroraClient.lastCommandTimestamp = Date.now();
|
AuroraClient.lastCommandTimestamp = Date.now();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(String(error));
|
logger.error("bot", `Error executing command ${interaction.commandName}`, error);
|
||||||
const errorEmbed = createErrorEmbed('There was an error while executing this command!');
|
const errorEmbed = createErrorEmbed('There was an error while executing this command!');
|
||||||
|
|
||||||
if (interaction.replied || interaction.deferred) {
|
if (interaction.replied || interaction.deferred) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ButtonInteraction, StringSelectMenuInteraction, ModalSubmitInteraction,
|
|||||||
|
|
||||||
import { UserError } from "@shared/lib/errors";
|
import { UserError } from "@shared/lib/errors";
|
||||||
import { createErrorEmbed } from "@lib/embeds";
|
import { createErrorEmbed } from "@lib/embeds";
|
||||||
|
import { logger } from "@shared/lib/logger";
|
||||||
|
|
||||||
type ComponentInteraction = ButtonInteraction | StringSelectMenuInteraction | ModalSubmitInteraction;
|
type ComponentInteraction = ButtonInteraction | StringSelectMenuInteraction | ModalSubmitInteraction;
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ export class ComponentInteractionHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(`Handler method ${route.method} not found in module`);
|
logger.error("bot", `Handler method ${route.method} not found in module`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,7 @@ export class ComponentInteractionHandler {
|
|||||||
|
|
||||||
// Log system errors (non-user errors) for debugging
|
// Log system errors (non-user errors) for debugging
|
||||||
if (!isUserError) {
|
if (!isUserError) {
|
||||||
console.error(`Error in ${handlerName}:`, error);
|
logger.error("bot", `Error in ${handlerName}`, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorEmbed = createErrorEmbed(errorMessage);
|
const errorEmbed = createErrorEmbed(errorMessage);
|
||||||
@@ -72,7 +73,7 @@ export class ComponentInteractionHandler {
|
|||||||
}
|
}
|
||||||
} catch (replyError) {
|
} catch (replyError) {
|
||||||
// If we can't send a reply, log it
|
// If we can't send a reply, log it
|
||||||
console.error(`Failed to send error response in ${handlerName}:`, replyError);
|
logger.error("bot", `Failed to send error response in ${handlerName}`, replyError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
118
shared/lib/logger.test.ts
Normal file
118
shared/lib/logger.test.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { expect, test, describe, beforeAll, afterAll, spyOn } from "bun:test";
|
||||||
|
import { logger } from "./logger";
|
||||||
|
import { existsSync, unlinkSync, readFileSync, writeFileSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
describe("Logger", () => {
|
||||||
|
const logDir = join(process.cwd(), "logs");
|
||||||
|
const logFile = join(logDir, "error.log");
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
// Cleanup if exists
|
||||||
|
try {
|
||||||
|
if (existsSync(logFile)) unlinkSync(logFile);
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should log info messages to console with correct format", () => {
|
||||||
|
const spy = spyOn(console, "log");
|
||||||
|
const message = "Formatting test";
|
||||||
|
logger.info("system", message);
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
const callArgs = spy.mock.calls[0]?.[0];
|
||||||
|
expect(callArgs).toBeDefined();
|
||||||
|
if (callArgs) {
|
||||||
|
// Strict regex check for ISO timestamp and format
|
||||||
|
const regex = /^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[INFO\] \[SYSTEM\] Formatting test$/;
|
||||||
|
expect(callArgs).toMatch(regex);
|
||||||
|
}
|
||||||
|
spy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should write error logs to file with stack trace", async () => {
|
||||||
|
const errorMessage = "Test error message";
|
||||||
|
const testError = new Error("Source error");
|
||||||
|
|
||||||
|
logger.error("system", errorMessage, testError);
|
||||||
|
|
||||||
|
// Polling for file write instead of fixed timeout
|
||||||
|
let content = "";
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
if (existsSync(logFile)) {
|
||||||
|
content = readFileSync(logFile, "utf-8");
|
||||||
|
if (content.includes("Source error")) break;
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(content).toMatch(/^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[ERROR\] \[SYSTEM\] Test error message: Source error/);
|
||||||
|
expect(content).toContain("Stack Trace:");
|
||||||
|
expect(content).toContain("Error: Source error");
|
||||||
|
expect(content).toContain("logger.test.ts");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle log directory creation failures gracefully", async () => {
|
||||||
|
const consoleSpy = spyOn(console, "error");
|
||||||
|
|
||||||
|
// We trigger an error by trying to use a path that is a file where a directory should be
|
||||||
|
const triggerFile = join(process.cwd(), "logs_fail_trigger");
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeFileSync(triggerFile, "not a directory");
|
||||||
|
|
||||||
|
// Manually override paths for this test instance
|
||||||
|
const originalLogDir = (logger as any).logDir;
|
||||||
|
const originalLogPath = (logger as any).errorLogPath;
|
||||||
|
|
||||||
|
(logger as any).logDir = triggerFile;
|
||||||
|
(logger as any).errorLogPath = join(triggerFile, "error.log");
|
||||||
|
(logger as any).initialized = false;
|
||||||
|
|
||||||
|
logger.error("system", "This should fail directory creation");
|
||||||
|
|
||||||
|
// Wait for async initialization attempt
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
expect(consoleSpy).toHaveBeenCalled();
|
||||||
|
expect(consoleSpy.mock.calls.some(call =>
|
||||||
|
String(call[0]).includes("Failed to initialize logger directory")
|
||||||
|
)).toBe(true);
|
||||||
|
|
||||||
|
// Reset logger state
|
||||||
|
(logger as any).logDir = originalLogDir;
|
||||||
|
(logger as any).errorLogPath = originalLogPath;
|
||||||
|
(logger as any).initialized = false;
|
||||||
|
} finally {
|
||||||
|
if (existsSync(triggerFile)) unlinkSync(triggerFile);
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should include complex data objects in logs", () => {
|
||||||
|
const spy = spyOn(console, "log");
|
||||||
|
const data = { userId: "123", tags: ["test"] };
|
||||||
|
logger.info("bot", "Message with data", data);
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
const callArgs = spy.mock.calls[0]?.[0];
|
||||||
|
expect(callArgs).toBeDefined();
|
||||||
|
if (callArgs) {
|
||||||
|
expect(callArgs).toContain(` | Data: ${JSON.stringify(data)}`);
|
||||||
|
}
|
||||||
|
spy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle circular references in data objects", () => {
|
||||||
|
const spy = spyOn(console, "log");
|
||||||
|
const data: any = { name: "circular" };
|
||||||
|
data.self = data;
|
||||||
|
|
||||||
|
logger.info("bot", "Circular test", data);
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
const callArgs = spy.mock.calls[0]?.[0];
|
||||||
|
expect(callArgs).toContain("[Circular]");
|
||||||
|
spy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
162
shared/lib/logger.ts
Normal file
162
shared/lib/logger.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { join, resolve } from "path";
|
||||||
|
import { appendFile, mkdir, stat } from "fs/promises";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
|
||||||
|
export enum LogLevel {
|
||||||
|
DEBUG = 0,
|
||||||
|
INFO = 1,
|
||||||
|
WARN = 2,
|
||||||
|
ERROR = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
const LogLevelNames = {
|
||||||
|
[LogLevel.DEBUG]: "DEBUG",
|
||||||
|
[LogLevel.INFO]: "INFO",
|
||||||
|
[LogLevel.WARN]: "WARN",
|
||||||
|
[LogLevel.ERROR]: "ERROR",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LogSource = "bot" | "web" | "shared" | "system";
|
||||||
|
|
||||||
|
export interface LogEntry {
|
||||||
|
timestamp: string;
|
||||||
|
level: string;
|
||||||
|
source: LogSource;
|
||||||
|
message: string;
|
||||||
|
data?: any;
|
||||||
|
stack?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
private logDir: string;
|
||||||
|
private errorLogPath: string;
|
||||||
|
private initialized: boolean = false;
|
||||||
|
private initPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Use resolve with __dirname or process.cwd() but make it more robust
|
||||||
|
// Since this is in shared/lib/, we can try to find the project root
|
||||||
|
// For now, let's stick to a resolved path from process.cwd() or a safer alternative
|
||||||
|
this.logDir = resolve(process.cwd(), "logs");
|
||||||
|
this.errorLogPath = join(this.logDir, "error.log");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureInitialized() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
if (this.initPromise) return this.initPromise;
|
||||||
|
|
||||||
|
this.initPromise = (async () => {
|
||||||
|
try {
|
||||||
|
await mkdir(this.logDir, { recursive: true });
|
||||||
|
this.initialized = true;
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.code === "EEXIST" || err.code === "ENOTDIR") {
|
||||||
|
try {
|
||||||
|
const stats = await stat(this.logDir);
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
this.initialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (statErr) {}
|
||||||
|
}
|
||||||
|
console.error(`[SYSTEM] Failed to initialize logger directory at ${this.logDir}:`, err);
|
||||||
|
} finally {
|
||||||
|
this.initPromise = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return this.initPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private safeStringify(data: any): string {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(data);
|
||||||
|
} catch (err) {
|
||||||
|
const seen = new WeakSet();
|
||||||
|
return JSON.stringify(data, (key, value) => {
|
||||||
|
if (typeof value === "object" && value !== null) {
|
||||||
|
if (seen.has(value)) return "[Circular]";
|
||||||
|
seen.add(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatMessage(entry: LogEntry): string {
|
||||||
|
const dataStr = entry.data ? ` | Data: ${this.safeStringify(entry.data)}` : "";
|
||||||
|
const stackStr = entry.stack ? `\nStack Trace:\n${entry.stack}` : "";
|
||||||
|
return `[${entry.timestamp}] [${entry.level}] [${entry.source.toUpperCase()}] ${entry.message}${dataStr}${stackStr}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async writeToErrorLog(formatted: string) {
|
||||||
|
await this.ensureInitialized();
|
||||||
|
try {
|
||||||
|
await appendFile(this.errorLogPath, formatted + "\n");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[SYSTEM] Failed to write to error log file:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private log(level: LogLevel, source: LogSource, message: string, errorOrData?: any) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const levelName = LogLevelNames[level];
|
||||||
|
|
||||||
|
const entry: LogEntry = {
|
||||||
|
timestamp,
|
||||||
|
level: levelName,
|
||||||
|
source,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (level === LogLevel.ERROR && errorOrData instanceof Error) {
|
||||||
|
entry.stack = errorOrData.stack;
|
||||||
|
entry.message = `${message}: ${errorOrData.message}`;
|
||||||
|
} else if (errorOrData !== undefined) {
|
||||||
|
entry.data = errorOrData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = this.formatMessage(entry);
|
||||||
|
|
||||||
|
// Print to console
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel.DEBUG:
|
||||||
|
console.debug(formatted);
|
||||||
|
break;
|
||||||
|
case LogLevel.INFO:
|
||||||
|
console.log(formatted);
|
||||||
|
break;
|
||||||
|
case LogLevel.WARN:
|
||||||
|
console.warn(formatted);
|
||||||
|
break;
|
||||||
|
case LogLevel.ERROR:
|
||||||
|
console.error(formatted);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persistent error logging
|
||||||
|
if (level === LogLevel.ERROR) {
|
||||||
|
this.writeToErrorLog(formatted).catch(() => {
|
||||||
|
// Silently fail to avoid infinite loops
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(source: LogSource, message: string, data?: any) {
|
||||||
|
this.log(LogLevel.DEBUG, source, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
info(source: LogSource, message: string, data?: any) {
|
||||||
|
this.log(LogLevel.INFO, source, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(source: LogSource, message: string, data?: any) {
|
||||||
|
this.log(LogLevel.WARN, source, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(source: LogSource, message: string, error?: any) {
|
||||||
|
this.log(LogLevel.ERROR, source, message, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logger = new Logger();
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { serve, spawn, type Subprocess } from "bun";
|
import { serve, spawn, type Subprocess } from "bun";
|
||||||
import { join, resolve, dirname } from "path";
|
import { join, resolve, dirname } from "path";
|
||||||
|
import { logger } from "@shared/lib/logger";
|
||||||
|
|
||||||
export interface WebServerConfig {
|
export interface WebServerConfig {
|
||||||
port?: number;
|
port?: number;
|
||||||
@@ -39,7 +40,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
|||||||
const isDev = process.env.NODE_ENV !== "production";
|
const isDev = process.env.NODE_ENV !== "production";
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
console.log("🛠️ Starting Web Bundler in Watch Mode...");
|
logger.info("web", "Starting Web Bundler in Watch Mode...");
|
||||||
try {
|
try {
|
||||||
buildProcess = spawn(["bun", "run", "build.ts", "--watch"], {
|
buildProcess = spawn(["bun", "run", "build.ts", "--watch"], {
|
||||||
cwd: webRoot,
|
cwd: webRoot,
|
||||||
@@ -47,7 +48,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
|||||||
stderr: "inherit",
|
stderr: "inherit",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to start build process:", error);
|
logger.error("web", "Failed to start build process", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
|||||||
// Security Check: limit concurrent connections
|
// Security Check: limit concurrent connections
|
||||||
const currentConnections = server.pendingWebSockets;
|
const currentConnections = server.pendingWebSockets;
|
||||||
if (currentConnections >= MAX_CONNECTIONS) {
|
if (currentConnections >= MAX_CONNECTIONS) {
|
||||||
console.warn(`⚠️ [WS] Connection rejected: limit reached (${currentConnections}/${MAX_CONNECTIONS})`);
|
logger.warn("web", `Connection rejected: limit reached (${currentConnections}/${MAX_CONNECTIONS})`);
|
||||||
return new Response("Connection limit reached", { status: 429 });
|
return new Response("Connection limit reached", { status: 429 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +95,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
|||||||
const stats = await getFullDashboardStats();
|
const stats = await getFullDashboardStats();
|
||||||
return Response.json(stats);
|
return Response.json(stats);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching dashboard stats:", error);
|
logger.error("web", "Error fetching dashboard stats", error);
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ error: "Failed to fetch dashboard statistics" },
|
{ error: "Failed to fetch dashboard statistics" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
@@ -124,7 +125,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
|||||||
const activity = await activityPromise;
|
const activity = await activityPromise;
|
||||||
return Response.json(activity);
|
return Response.json(activity);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching activity stats:", error);
|
logger.error("web", "Error fetching activity stats", error);
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ error: "Failed to fetch activity statistics" },
|
{ error: "Failed to fetch activity statistics" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
@@ -160,7 +161,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
|||||||
return Response.json(result);
|
return Response.json(result);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error executing administrative action:", error);
|
logger.error("web", "Error executing administrative action", error);
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ error: "Failed to execute administrative action" },
|
{ error: "Failed to execute administrative action" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
@@ -196,7 +197,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
|||||||
return Response.json({ success: true });
|
return Response.json({ success: true });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Settings error:", error);
|
logger.error("web", "Settings error", error);
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ error: "Failed to process settings request", details: error instanceof Error ? error.message : String(error) },
|
{ error: "Failed to process settings request", details: error instanceof Error ? error.message : String(error) },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
@@ -235,7 +236,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
|||||||
|
|
||||||
return Response.json({ roles, channels, commands });
|
return Response.json({ roles, channels, commands });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching settings meta:", error);
|
logger.error("web", "Error fetching settings meta", error);
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ error: "Failed to fetch metadata" },
|
{ error: "Failed to fetch metadata" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
@@ -294,7 +295,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
|||||||
websocket: {
|
websocket: {
|
||||||
open(ws) {
|
open(ws) {
|
||||||
ws.subscribe("dashboard");
|
ws.subscribe("dashboard");
|
||||||
console.log(`🔌 [WS] Client connected. Total: ${server.pendingWebSockets}`);
|
logger.debug("web", `Client connected. Total: ${server.pendingWebSockets}`);
|
||||||
|
|
||||||
// Send initial stats
|
// Send initial stats
|
||||||
getFullDashboardStats().then(stats => {
|
getFullDashboardStats().then(stats => {
|
||||||
@@ -308,7 +309,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
|||||||
const stats = await getFullDashboardStats();
|
const stats = await getFullDashboardStats();
|
||||||
server.publish("dashboard", JSON.stringify({ type: "STATS_UPDATE", data: stats }));
|
server.publish("dashboard", JSON.stringify({ type: "STATS_UPDATE", data: stats }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in stats broadcast:", error);
|
logger.error("web", "Error in stats broadcast", error);
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
@@ -319,7 +320,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
|||||||
|
|
||||||
// Defense-in-depth: redundant length check before parsing
|
// Defense-in-depth: redundant length check before parsing
|
||||||
if (messageStr.length > MAX_PAYLOAD_BYTES) {
|
if (messageStr.length > MAX_PAYLOAD_BYTES) {
|
||||||
console.error("❌ [WS] Payload exceeded maximum limit");
|
logger.error("web", "Payload exceeded maximum limit");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,7 +329,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
|||||||
const parsed = WsMessageSchema.safeParse(rawData);
|
const parsed = WsMessageSchema.safeParse(rawData);
|
||||||
|
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
console.error("❌ [WS] Invalid message format:", parsed.error.issues);
|
logger.error("web", "Invalid message format", parsed.error.issues);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,12 +337,12 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
|||||||
ws.send(JSON.stringify({ type: "PONG" }));
|
ws.send(JSON.stringify({ type: "PONG" }));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("❌ [WS] Failed to handle message:", e instanceof Error ? e.message : "Malformed JSON");
|
logger.error("web", "Failed to handle message", e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
close(ws) {
|
close(ws) {
|
||||||
ws.unsubscribe("dashboard");
|
ws.unsubscribe("dashboard");
|
||||||
console.log(`🔌 [WS] Client disconnected. Total remaining: ${server.pendingWebSockets}`);
|
logger.debug("web", `Client disconnected. Total remaining: ${server.pendingWebSockets}`);
|
||||||
|
|
||||||
// Stop broadcast interval if no clients left
|
// Stop broadcast interval if no clients left
|
||||||
if (server.pendingWebSockets === 0 && statsBroadcastInterval) {
|
if (server.pendingWebSockets === 0 && statsBroadcastInterval) {
|
||||||
@@ -382,7 +383,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
|||||||
// Helper to unwrap result or return default
|
// Helper to unwrap result or return default
|
||||||
const unwrap = <T>(result: PromiseSettledResult<T>, defaultValue: T, name: string): T => {
|
const unwrap = <T>(result: PromiseSettledResult<T>, defaultValue: T, name: string): T => {
|
||||||
if (result.status === 'fulfilled') return result.value;
|
if (result.status === 'fulfilled') return result.value;
|
||||||
console.error(`Failed to fetch ${name}:`, result.reason);
|
logger.error("web", `Failed to fetch ${name}`, result.reason);
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -403,7 +404,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
|||||||
const recentEvents = unwrap(results[4], [], 'recentEvents');
|
const recentEvents = unwrap(results[4], [], 'recentEvents');
|
||||||
const totalItems = unwrap(results[5], 0, 'totalItems');
|
const totalItems = unwrap(results[5], 0, 'totalItems');
|
||||||
const activeLootdrops = unwrap(results[6], [], 'activeLootdrops');
|
const activeLootdrops = unwrap(results[6], [], 'activeLootdrops');
|
||||||
const leaderboards = unwrap(results[7], { topLevels: [], topWealth: [] }, 'leaderboards');
|
const leaderboards = unwrap(results[7], { topLevels: [], topWealth: [], topNetWorth: [] }, 'leaderboards');
|
||||||
const lootdropState = unwrap(results[8], undefined, 'lootdropState');
|
const lootdropState = unwrap(results[8], undefined, 'lootdropState');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user