diff --git a/bot/index.ts b/bot/index.ts index e573c4a..f7b149b 100644 --- a/bot/index.ts +++ b/bot/index.ts @@ -8,6 +8,7 @@ import { startWebServerFromRoot } from "../web/src/server"; await AuroraClient.loadCommands(); await AuroraClient.loadEvents(); await AuroraClient.deployCommands(); +await AuroraClient.setupSystemEvents(); console.log("๐ŸŒ Starting web server..."); diff --git a/bot/lib/BotClient.test.ts b/bot/lib/BotClient.test.ts index da2f13c..6408a41 100644 --- a/bot/lib/BotClient.test.ts +++ b/bot/lib/BotClient.test.ts @@ -38,6 +38,15 @@ mock.module("../lib/loaders/EventLoader", () => ({ })); // Mock dashboard service to prevent network/db calls during event handling +mock.module("@shared/modules/economy/lootdrop.service", () => ({ + lootdropService: { clearCaches: mock(async () => { }) } +})); +mock.module("@shared/modules/trade/trade.service", () => ({ + tradeService: { clearSessions: mock(() => { }) } +})); +mock.module("@/modules/admin/item_wizard", () => ({ + clearDraftSessions: mock(() => { }) +})); mock.module("@shared/modules/dashboard/dashboard.service", () => ({ dashboardService: { recordEvent: mock(() => Promise.resolve()) @@ -48,11 +57,12 @@ describe("AuroraClient System Events", () => { let AuroraClient: any; beforeEach(async () => { - // Clear mocks and re-import client to ensure fresh listeners if possible - // Note: AuroraClient is a singleton, so we mostly reset its state + systemEvents.removeAllListeners(); const module = await import("./BotClient"); AuroraClient = module.AuroraClient; AuroraClient.maintenanceMode = false; + // MUST call explicitly now + await AuroraClient.setupSystemEvents(); }); /** @@ -61,14 +71,11 @@ describe("AuroraClient System Events", () => { */ test("should toggle maintenanceMode when MAINTENANCE_MODE event is received", async () => { systemEvents.emit(EVENTS.ACTIONS.MAINTENANCE_MODE, { enabled: true, reason: "Testing" }); - - // Give event loop time to process - await new Promise(resolve => setTimeout(resolve, 20)); - + await new Promise(resolve => setTimeout(resolve, 30)); expect(AuroraClient.maintenanceMode).toBe(true); systemEvents.emit(EVENTS.ACTIONS.MAINTENANCE_MODE, { enabled: false }); - await new Promise(resolve => setTimeout(resolve, 20)); + await new Promise(resolve => setTimeout(resolve, 30)); expect(AuroraClient.maintenanceMode).toBe(false); }); @@ -77,16 +84,28 @@ describe("AuroraClient System Events", () => { * Requirement: loadCommands and deployCommands should be called */ test("should reload commands when RELOAD_COMMANDS event is received", async () => { - // Spy on the methods that should be called const loadSpy = spyOn(AuroraClient, "loadCommands").mockImplementation(() => Promise.resolve()); const deploySpy = spyOn(AuroraClient, "deployCommands").mockImplementation(() => Promise.resolve()); systemEvents.emit(EVENTS.ACTIONS.RELOAD_COMMANDS); - - // Wait for async handlers await new Promise(resolve => setTimeout(resolve, 50)); expect(loadSpy).toHaveBeenCalled(); expect(deploySpy).toHaveBeenCalled(); }); + + /** + * Test Case: Cache Clearance + * Requirement: Service clear methods should be triggered + */ + test("should trigger service cache clearance when CLEAR_CACHE is received", async () => { + const { lootdropService } = await import("@shared/modules/economy/lootdrop.service"); + const { tradeService } = await import("@shared/modules/trade/trade.service"); + + systemEvents.emit(EVENTS.ACTIONS.CLEAR_CACHE); + await new Promise(resolve => setTimeout(resolve, 50)); + + expect(lootdropService.clearCaches).toHaveBeenCalled(); + expect(tradeService.clearSessions).toHaveBeenCalled(); + }); }); diff --git a/bot/lib/BotClient.ts b/bot/lib/BotClient.ts index e763e15..a394f6d 100644 --- a/bot/lib/BotClient.ts +++ b/bot/lib/BotClient.ts @@ -18,13 +18,14 @@ export class Client extends DiscordClient { this.commands = new Collection(); this.commandLoader = new CommandLoader(this); this.eventLoader = new EventLoader(this); - this.setupSystemEvents(); } - private setupSystemEvents() { - import("@shared/lib/events").then(({ systemEvents, EVENTS }) => { - systemEvents.on(EVENTS.ACTIONS.RELOAD_COMMANDS, async () => { - console.log("๐Ÿ”„ System Action: Reloading commands..."); + public async setupSystemEvents() { + const { systemEvents, EVENTS } = await import("@shared/lib/events"); + + systemEvents.on(EVENTS.ACTIONS.RELOAD_COMMANDS, async () => { + console.log("๐Ÿ”„ System Action: Reloading commands..."); + try { await this.loadCommands(true); await this.deployCommands(); @@ -34,24 +35,42 @@ export class Client extends DiscordClient { message: "Bot: Commands reloaded and redeployed", icon: "โœ…" }); - }); + } catch (error) { + console.error("Failed to reload commands:", error); + } + }); + + systemEvents.on(EVENTS.ACTIONS.CLEAR_CACHE, async () => { + console.log("๏ฟฝ System Action: Clearing all internal caches..."); + + try { + // 1. Lootdrop Service + const { lootdropService } = await import("@shared/modules/economy/lootdrop.service"); + await lootdropService.clearCaches(); + + // 2. Trade Service + const { tradeService } = await import("@shared/modules/trade/trade.service"); + tradeService.clearSessions(); + + // 3. Item Wizard + const { clearDraftSessions } = await import("@/modules/admin/item_wizard"); + clearDraftSessions(); - systemEvents.on(EVENTS.ACTIONS.CLEAR_CACHE, async () => { - console.log("๐Ÿงน System Action: Clearing caches..."); - // In a real app, we'd loop through services and clear their internal maps const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service"); await dashboardService.recordEvent({ type: "success", - message: "Bot: Internal caches cleared", + message: "Bot: All internal caches and sessions cleared", icon: "๐Ÿงผ" }); - }); + } catch (error) { + console.error("Failed to clear caches:", error); + } + }); - systemEvents.on(EVENTS.ACTIONS.MAINTENANCE_MODE, async (data: { enabled: boolean, reason?: string }) => { - const { enabled, reason } = data; - console.log(`๐Ÿ› ๏ธ System Action: Maintenance mode ${enabled ? "ON" : "OFF"}${reason ? ` (${reason})` : ""}`); - this.maintenanceMode = enabled; - }); + systemEvents.on(EVENTS.ACTIONS.MAINTENANCE_MODE, async (data: { enabled: boolean, reason?: string }) => { + const { enabled, reason } = data; + console.log(`๐Ÿ› ๏ธ System Action: Maintenance mode ${enabled ? "ON" : "OFF"}${reason ? ` (${reason})` : ""}`); + this.maintenanceMode = enabled; }); } diff --git a/bot/modules/admin/item_wizard.ts b/bot/modules/admin/item_wizard.ts index 228f6aa..7e18889 100644 --- a/bot/modules/admin/item_wizard.ts +++ b/bot/modules/admin/item_wizard.ts @@ -241,3 +241,8 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => { } }; + +export const clearDraftSessions = () => { + draftSession.clear(); + console.log("[ItemWizard] All draft item creation sessions cleared."); +}; diff --git a/shared/lib/env.ts b/shared/lib/env.ts index 93ea2ae..625b35c 100644 --- a/shared/lib/env.ts +++ b/shared/lib/env.ts @@ -7,6 +7,7 @@ const envSchema = z.object({ DATABASE_URL: z.string().min(1, "Database URL is required"), PORT: z.coerce.number().default(3000), HOST: z.string().default("127.0.0.1"), + ADMIN_TOKEN: z.string().min(8, "ADMIN_TOKEN must be at least 8 characters"), }); const parsedEnv = envSchema.safeParse(process.env); diff --git a/shared/modules/dashboard/dashboard.types.ts b/shared/modules/dashboard/dashboard.types.ts index 297830e..96f7921 100644 --- a/shared/modules/dashboard/dashboard.types.ts +++ b/shared/modules/dashboard/dashboard.types.ts @@ -59,6 +59,12 @@ export const ClientStatsSchema = z.object({ export type ClientStats = z.infer; +// Action Schemas +export const MaintenanceModeSchema = z.object({ + enabled: z.boolean(), + reason: z.string().optional(), +}); + // WebSocket Message Schemas export const WsMessageSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal("PING") }), diff --git a/shared/modules/economy/lootdrop.service.ts b/shared/modules/economy/lootdrop.service.ts index 171684e..6584dbc 100644 --- a/shared/modules/economy/lootdrop.service.ts +++ b/shared/modules/economy/lootdrop.service.ts @@ -163,6 +163,11 @@ class LootdropService { return { success: false, error: "An error occurred while processing the reward." }; } } + public async clearCaches() { + this.channelActivity.clear(); + this.channelCooldowns.clear(); + console.log("[LootdropService] Caches cleared via administrative action."); + } } export const lootdropService = new LootdropService(); diff --git a/shared/modules/trade/trade.service.ts b/shared/modules/trade/trade.service.ts index e5805e3..86f63f6 100644 --- a/shared/modules/trade/trade.service.ts +++ b/shared/modules/trade/trade.service.ts @@ -196,5 +196,10 @@ export const tradeService = { }); tradeService.endSession(threadId); + }, + + clearSessions: () => { + sessions.clear(); + console.log("[TradeService] All active trade sessions cleared."); } }; diff --git a/web/src/components/ControlPanel.tsx b/web/src/components/ControlPanel.tsx index 6cd7dd9..8334734 100644 --- a/web/src/components/ControlPanel.tsx +++ b/web/src/components/ControlPanel.tsx @@ -10,6 +10,14 @@ interface ControlPanelProps { maintenanceMode: boolean; } +declare global { + interface Window { + AURORA_ENV?: { + ADMIN_TOKEN: string; + }; + } +} + /** * ControlPanel component provides quick administrative actions for the bot. * Integrated with the premium glassmorphic theme. @@ -20,12 +28,16 @@ export function ControlPanel({ maintenanceMode }: ControlPanelProps) { /** * Handles triggering an administrative action via the API */ - const handleAction = async (action: string, payload?: any) => { + const handleAction = async (action: string, payload?: Record) => { setLoading(action); try { + const token = window.AURORA_ENV?.ADMIN_TOKEN; const response = await fetch(`/api/actions/${action}`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}` + }, body: payload ? JSON.stringify(payload) : undefined, }); if (!response.ok) throw new Error(`Action ${action} failed`); @@ -88,8 +100,8 @@ export function ControlPanel({ maintenanceMode }: ControlPanelProps) {