feat: implement administrative control panel with real-time bot actions
This commit is contained in:
@@ -12,5 +12,10 @@ export const EVENTS = {
|
||||
DASHBOARD: {
|
||||
STATS_UPDATE: "dashboard:stats_update",
|
||||
NEW_EVENT: "dashboard:new_event",
|
||||
},
|
||||
ACTIONS: {
|
||||
RELOAD_COMMANDS: "actions:reload_commands",
|
||||
CLEAR_CACHE: "actions:clear_cache",
|
||||
MAINTENANCE_MODE: "actions:maintenance_mode",
|
||||
}
|
||||
} as const;
|
||||
|
||||
66
shared/modules/admin/action.service.test.ts
Normal file
66
shared/modules/admin/action.service.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, test, mock, beforeEach, spyOn } from "bun:test";
|
||||
import { actionService } from "./action.service";
|
||||
import { systemEvents, EVENTS } from "@shared/lib/events";
|
||||
import { dashboardService } from "@shared/modules/dashboard/dashboard.service";
|
||||
|
||||
describe("ActionService", () => {
|
||||
beforeEach(() => {
|
||||
// Clear any previous mock state
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test Case: Command Reload
|
||||
* Requirement: Emits event and records to dashboard
|
||||
*/
|
||||
test("reloadCommands should emit RELOAD_COMMANDS event and record dashboard event", async () => {
|
||||
const emitSpy = spyOn(systemEvents, "emit");
|
||||
const recordSpy = spyOn(dashboardService, "recordEvent").mockImplementation(() => Promise.resolve());
|
||||
|
||||
const result = await actionService.reloadCommands();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(emitSpy).toHaveBeenCalledWith(EVENTS.ACTIONS.RELOAD_COMMANDS);
|
||||
expect(recordSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: "info",
|
||||
message: "Admin: Triggered command reload"
|
||||
}));
|
||||
});
|
||||
|
||||
/**
|
||||
* Test Case: Cache Clearance
|
||||
* Requirement: Emits event and records to dashboard
|
||||
*/
|
||||
test("clearCache should emit CLEAR_CACHE event and record dashboard event", async () => {
|
||||
const emitSpy = spyOn(systemEvents, "emit");
|
||||
const recordSpy = spyOn(dashboardService, "recordEvent").mockImplementation(() => Promise.resolve());
|
||||
|
||||
const result = await actionService.clearCache();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(emitSpy).toHaveBeenCalledWith(EVENTS.ACTIONS.CLEAR_CACHE);
|
||||
expect(recordSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: "info",
|
||||
message: "Admin: Triggered cache clearance"
|
||||
}));
|
||||
});
|
||||
|
||||
/**
|
||||
* Test Case: Maintenance Mode Toggle
|
||||
* Requirement: Emits event with correct payload and records to dashboard with warning type
|
||||
*/
|
||||
test("toggleMaintenanceMode should emit MAINTENANCE_MODE event and record dashboard event", async () => {
|
||||
const emitSpy = spyOn(systemEvents, "emit");
|
||||
const recordSpy = spyOn(dashboardService, "recordEvent").mockImplementation(() => Promise.resolve());
|
||||
|
||||
const result = await actionService.toggleMaintenanceMode(true, "Test Reason");
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.enabled).toBe(true);
|
||||
expect(emitSpy).toHaveBeenCalledWith(EVENTS.ACTIONS.MAINTENANCE_MODE, { enabled: true, reason: "Test Reason" });
|
||||
expect(recordSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: "warn",
|
||||
message: "Admin: Maintenance mode ENABLED (Test Reason)"
|
||||
}));
|
||||
});
|
||||
});
|
||||
53
shared/modules/admin/action.service.ts
Normal file
53
shared/modules/admin/action.service.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { systemEvents, EVENTS } from "@shared/lib/events";
|
||||
import { dashboardService } from "@shared/modules/dashboard/dashboard.service";
|
||||
|
||||
/**
|
||||
* Service to handle administrative actions triggered from the dashboard.
|
||||
* These actions are broadcasted to the bot via the system event bus.
|
||||
*/
|
||||
export const actionService = {
|
||||
/**
|
||||
* Triggers a reload of all bot commands.
|
||||
*/
|
||||
reloadCommands: async () => {
|
||||
systemEvents.emit(EVENTS.ACTIONS.RELOAD_COMMANDS);
|
||||
|
||||
await dashboardService.recordEvent({
|
||||
type: "info",
|
||||
message: "Admin: Triggered command reload",
|
||||
icon: "♻️"
|
||||
});
|
||||
|
||||
return { success: true, message: "Command reload triggered" };
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers a clearance of internal bot caches.
|
||||
*/
|
||||
clearCache: async () => {
|
||||
systemEvents.emit(EVENTS.ACTIONS.CLEAR_CACHE);
|
||||
|
||||
await dashboardService.recordEvent({
|
||||
type: "info",
|
||||
message: "Admin: Triggered cache clearance",
|
||||
icon: "🧹"
|
||||
});
|
||||
|
||||
return { success: true, message: "Cache clearance triggered" };
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles maintenance mode for the bot.
|
||||
*/
|
||||
toggleMaintenanceMode: async (enabled: boolean, reason?: string) => {
|
||||
systemEvents.emit(EVENTS.ACTIONS.MAINTENANCE_MODE, { enabled, reason });
|
||||
|
||||
await dashboardService.recordEvent({
|
||||
type: enabled ? "warn" : "info",
|
||||
message: `Admin: Maintenance mode ${enabled ? "ENABLED" : "DISABLED"}${reason ? ` (${reason})` : ""}`,
|
||||
icon: "🛠️"
|
||||
});
|
||||
|
||||
return { success: true, enabled, message: `Maintenance mode ${enabled ? "enabled" : "disabled"}` };
|
||||
}
|
||||
};
|
||||
@@ -121,7 +121,7 @@ export const dashboardService = {
|
||||
|
||||
// Combine and sort by timestamp
|
||||
const allEvents = [...txEvents, ...modEvents]
|
||||
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
|
||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||||
.slice(0, limit);
|
||||
|
||||
return allEvents;
|
||||
@@ -141,7 +141,7 @@ export const dashboardService = {
|
||||
const { systemEvents, EVENTS } = await import("@shared/lib/events");
|
||||
systemEvents.emit(EVENTS.DASHBOARD.NEW_EVENT, {
|
||||
...fullEvent,
|
||||
timestamp: fullEvent.timestamp.toISOString()
|
||||
timestamp: (fullEvent.timestamp as Date).toISOString()
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to emit system event:", e);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const RecentEventSchema = z.object({
|
||||
type: z.enum(['success', 'error', 'info']),
|
||||
type: z.enum(['success', 'error', 'info', 'warn']),
|
||||
message: z.string(),
|
||||
timestamp: z.union([z.date(), z.string().datetime()]),
|
||||
icon: z.string().optional(),
|
||||
@@ -39,6 +39,7 @@ export const DashboardStatsSchema = z.object({
|
||||
recentEvents: z.array(RecentEventSchema),
|
||||
uptime: z.number(),
|
||||
lastCommandTimestamp: z.number().nullable(),
|
||||
maintenanceMode: z.boolean(),
|
||||
});
|
||||
|
||||
export type DashboardStats = z.infer<typeof DashboardStatsSchema>;
|
||||
|
||||
Reference in New Issue
Block a user