feat(dashboard): expand stats & remove admin token auth

This commit is contained in:
syntaxbullet
2026-01-08 22:14:13 +01:00
parent cf4c28e1df
commit d46434de18
10 changed files with 257 additions and 330 deletions

View File

@@ -7,7 +7,6 @@ DISCORD_BOT_TOKEN=your-discord-bot-token
DISCORD_CLIENT_ID=your-discord-client-id
DISCORD_GUILD_ID=your-discord-guild-id
DATABASE_URL=postgres://aurora:aurora@db:5432/aurora
ADMIN_TOKEN=Ffeg4hgsdfvsnyms,kmeuy64sy5y
VPS_USER=your-vps-user
VPS_HOST=your-vps-ip

View File

@@ -7,7 +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"),
ADMIN_TOKEN: z.string().min(8, "ADMIN_TOKEN must be at least 8 characters").optional(),
});
const parsedEnv = envSchema.safeParse(process.env);

View File

@@ -1,292 +1,99 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
import { dashboardService } from "./dashboard.service";
// Mock DrizzleClient
const mockSelect = mock(() => ({
from: mock(() => Promise.resolve([{ count: "5" }])),
}));
// Mock DrizzleClient before importing service
const mockFindMany = mock();
const mockLimit = mock();
const mockQuery = {
transactions: {
findMany: mock((): Promise<any[]> => Promise.resolve([])),
},
moderationCases: {
findMany: mock((): Promise<any[]> => Promise.resolve([])),
},
// Helper to support the chained calls in getLeaderboards
const mockChain = {
from: () => mockChain,
orderBy: () => mockChain,
limit: mockLimit
};
mock.module("@shared/db/DrizzleClient", () => ({
DrizzleClient: {
select: mockSelect,
query: mockQuery,
},
select: () => mockChain,
query: {
lootdrops: {
findMany: mockFindMany
}
}
}
}));
// Import service after mocking
import { dashboardService } from "./dashboard.service";
describe("dashboardService", () => {
beforeEach(() => {
mockSelect.mockClear();
mockQuery.transactions.findMany.mockClear();
mockQuery.moderationCases.findMany.mockClear();
// Reset default mock implementation
mockSelect.mockImplementation(() => ({
from: mock(() => Promise.resolve([{ count: "5" }])),
}));
mockFindMany.mockClear();
mockLimit.mockClear();
});
describe("getActiveUserCount", () => {
test("should return active user count from database", async () => {
mockSelect.mockImplementationOnce(() => ({
// @ts-ignore ts(2322)
from: mock(() => ({
where: mock(() => Promise.resolve([{ count: "5" }])),
})),
}));
const count = await dashboardService.getActiveUserCount();
expect(count).toBe(5);
expect(mockSelect).toHaveBeenCalled();
});
test("should return 0 when no users found", async () => {
mockSelect.mockImplementationOnce(() => ({
// @ts-ignore ts(2322)
from: mock(() => ({
where: mock(() => Promise.resolve([{ count: "0" }])),
})),
}));
const count = await dashboardService.getActiveUserCount();
expect(count).toBe(0);
});
});
describe("getTotalUserCount", () => {
test("should return total user count", async () => {
const count = await dashboardService.getTotalUserCount();
expect(count).toBe(5);
});
});
describe("getRecentTransactions", () => {
test("should return formatted transaction events", async () => {
const mockTx = [
describe("getActiveLootdrops", () => {
test("should return active lootdrops when found", async () => {
const mockDrops = [
{
type: "DAILY_REWARD",
description: "Daily reward",
messageId: "123",
channelId: "general",
rewardAmount: 100,
currency: "Gold",
createdAt: new Date(),
user: { username: "testuser" },
},
] as any;
mockQuery.transactions.findMany.mockResolvedValueOnce(mockTx);
const events = await dashboardService.getRecentTransactions(10);
expect(events).toHaveLength(1);
expect(events[0]?.type).toBe("info");
expect(events[0]?.message).toContain("testuser");
expect(events[0]?.icon).toBe("☀️");
});
test("should handle empty transactions", async () => {
mockQuery.transactions.findMany.mockResolvedValueOnce([]);
const events = await dashboardService.getRecentTransactions(10);
expect(events).toHaveLength(0);
});
});
describe("getRecentModerationCases", () => {
test("should return formatted moderation events", async () => {
const mockCases = [
{
type: "warn",
username: "baduser",
reason: "Spam",
createdAt: new Date(),
},
] as any;
mockQuery.moderationCases.findMany.mockResolvedValueOnce(mockCases);
const events = await dashboardService.getRecentModerationCases(10);
expect(events).toHaveLength(1);
expect(events[0]?.type).toBe("error");
expect(events[0]?.message).toContain("WARN");
expect(events[0]?.message).toContain("baduser");
expect(events[0]?.icon).toBe("⚠️");
});
});
describe("getRecentEvents", () => {
test("should combine and sort transactions and moderation events", async () => {
const now = new Date();
const earlier = new Date(now.getTime() - 1000);
mockQuery.transactions.findMany.mockResolvedValueOnce([
{
type: "DAILY_REWARD",
description: "Daily",
createdAt: now,
user: { username: "user1" },
},
] as unknown as any[]);
mockQuery.moderationCases.findMany.mockResolvedValueOnce([
{
type: "warn",
username: "user2",
reason: "Test",
createdAt: earlier,
},
] as unknown as any[]);
const events = await dashboardService.getRecentEvents(10);
expect(events).toHaveLength(2);
// Should be sorted by timestamp (newest first)
const t0 = events[0]?.timestamp instanceof Date ? events[0].timestamp.getTime() : new Date(events[0]?.timestamp ?? 0).getTime();
const t1 = events[1]?.timestamp instanceof Date ? events[1].timestamp.getTime() : new Date(events[1]?.timestamp ?? 0).getTime();
expect(t0).toBeGreaterThanOrEqual(t1);
});
});
describe("recordEvent", () => {
test("should emit NEW_EVENT to systemEvents", async () => {
const mockEmit = mock((_event: string, _data: unknown) => { });
mock.module("@shared/lib/events", () => ({
systemEvents: {
emit: mockEmit,
},
EVENTS: {
DASHBOARD: {
NEW_EVENT: "dashboard:new_event",
}
expiresAt: new Date(Date.now() + 3600000),
claimedBy: null
}
}));
];
mockFindMany.mockResolvedValue(mockDrops);
await dashboardService.recordEvent({
type: 'info',
message: 'Test Event',
icon: '🚀'
});
const result = await dashboardService.getActiveLootdrops();
expect(result).toEqual(mockDrops);
expect(mockFindMany).toHaveBeenCalledTimes(1);
});
expect(mockEmit).toHaveBeenCalled();
const calls = mockEmit.mock.calls;
if (calls.length > 0 && calls[0]) {
expect(calls[0][0]).toBe("dashboard:new_event");
const data = calls[0][1] as { message: string, timestamp: string };
expect(data.message).toBe("Test Event");
expect(data.timestamp).toBeDefined();
// Verify it's an ISO string
expect(() => new Date(data.timestamp).toISOString()).not.toThrow();
} else {
throw new Error("mockEmit was not called with expected arguments");
}
test("should return empty array if no active drops", async () => {
mockFindMany.mockResolvedValue([]);
const result = await dashboardService.getActiveLootdrops();
expect(result).toEqual([]);
});
});
describe("getActivityAggregation", () => {
test("should return exactly 24 data points representing the last 24 hours", async () => {
const now = new Date();
now.setHours(now.getHours(), 0, 0, 0);
describe("getLeaderboards", () => {
test("should combine top levels and wealth", async () => {
const mockTopLevels = [
{ username: "Alice", level: 10, avatar: "a.png" },
{ username: "Bob", level: 5, avatar: null },
{ username: "Charlie", level: 2, avatar: "c.png" }
];
const mockTopWealth = [
{ username: "Alice", balance: 1000n, avatar: "a.png" },
{ username: "Dave", balance: 500n, avatar: "d.png" },
{ username: "Bob", balance: 100n, avatar: null }
];
mockSelect.mockImplementationOnce(() => ({
// @ts-ignore
from: mock(() => ({
where: mock(() => ({
groupBy: mock(() => ({
orderBy: mock(() => Promise.resolve([
{
hour: now.toISOString(),
transactions: "10",
commands: "5"
}
]))
}))
}))
}))
}));
// Mock sequential calls to limit()
// First call is topLevels, second is topWealth
mockLimit
.mockResolvedValueOnce(mockTopLevels)
.mockResolvedValueOnce(mockTopWealth);
const activity = await dashboardService.getActivityAggregation();
const result = await dashboardService.getLeaderboards();
expect(activity).toHaveLength(24);
// Check if the current hour matches our mock
const currentHourData = activity.find(a => new Date(a.hour).getTime() === now.getTime());
expect(currentHourData).toBeDefined();
expect(currentHourData?.transactions).toBe(10);
expect(currentHourData?.commands).toBe(5);
// Check if missing hours are filled with 0
const otherHour = activity.find(a => new Date(a.hour).getTime() !== now.getTime());
expect(otherHour?.transactions).toBe(0);
expect(otherHour?.commands).toBe(0);
expect(result.topLevels).toEqual(mockTopLevels);
// Verify balance BigInt to string conversion
expect(result.topWealth).toHaveLength(3);
expect(result.topWealth[0]!.balance).toBe("1000");
expect(result.topWealth[0]!.username).toBe("Alice");
expect(result.topWealth[1]!.balance).toBe("500");
expect(mockLimit).toHaveBeenCalledTimes(2);
});
test("should return 24 hours of zeros if database is empty", async () => {
mockSelect.mockImplementationOnce(() => ({
// @ts-ignore
from: mock(() => ({
where: mock(() => ({
groupBy: mock(() => ({
orderBy: mock(() => Promise.resolve([]))
}))
}))
}))
}));
test("should handle empty leaderboards", async () => {
mockLimit.mockResolvedValue([]);
const activity = await dashboardService.getActivityAggregation();
expect(activity).toHaveLength(24);
expect(activity.every(a => a.transactions === 0 && a.commands === 0)).toBe(true);
});
test("should return 24 hours of zeros if database returns rows with null hours", async () => {
mockSelect.mockImplementationOnce(() => ({
// @ts-ignore
from: mock(() => ({
where: mock(() => ({
groupBy: mock(() => ({
orderBy: mock(() => Promise.resolve([{ hour: null, transactions: "10", commands: "5" }]))
}))
}))
}))
}));
const activity = await dashboardService.getActivityAggregation();
expect(activity).toHaveLength(24);
expect(activity.every(a => a.transactions === 0 && a.commands === 0)).toBe(true);
});
test("should correctly map hours regardless of input sort order", async () => {
const now = new Date();
now.setHours(now.getHours(), 0, 0, 0);
const hourAgo = new Date(now.getTime() - 60 * 60 * 1000);
mockSelect.mockImplementationOnce(() => ({
// @ts-ignore
from: mock(() => ({
where: mock(() => ({
groupBy: mock(() => ({
orderBy: mock(() => Promise.resolve([
{ hour: now.toISOString(), transactions: "10", commands: "5" },
{ hour: hourAgo.toISOString(), transactions: "20", commands: "10" }
]))
}))
}))
}))
}));
const activity = await dashboardService.getActivityAggregation();
const current = activity.find(a => a.hour === now.toISOString());
const past = activity.find(a => a.hour === hourAgo.toISOString());
expect(current?.transactions).toBe(10);
expect(past?.transactions).toBe(20);
const result = await dashboardService.getLeaderboards();
expect(result.topLevels).toEqual([]);
expect(result.topWealth).toEqual([]);
});
});
});

View File

@@ -1,5 +1,5 @@
import { DrizzleClient } from "@shared/db/DrizzleClient";
import { users, transactions, moderationCases, inventory, type User } from "@db/schema";
import { users, transactions, moderationCases, inventory, lootdrops, type User } from "@db/schema";
import { desc, sql, gte } from "drizzle-orm";
import type { RecentEvent, ActivityData } from "./dashboard.types";
import { TransactionType } from "@shared/lib/constants";
@@ -201,6 +201,44 @@ export const dashboardService = {
return activity;
},
/**
* Get active lootdrops
*/
getActiveLootdrops: async () => {
const activeDrops = await DrizzleClient.query.lootdrops.findMany({
where: (lootdrops, { isNull }) => isNull(lootdrops.claimedBy),
limit: 1,
orderBy: desc(lootdrops.createdAt)
});
return activeDrops;
},
/**
* Get leaderboards (Top 3 Levels and Wealth)
*/
getLeaderboards: async () => {
const topLevels = await DrizzleClient.select({
username: users.username,
level: users.level,
})
.from(users)
.orderBy(desc(users.level))
.limit(3);
const topWealth = await DrizzleClient.select({
username: users.username,
balance: users.balance,
})
.from(users)
.orderBy(desc(users.balance))
.limit(3);
return {
topLevels,
topWealth: topWealth.map(u => ({ ...u, balance: (u.balance || 0n).toString() }))
};
}
};
/**

View File

@@ -10,14 +10,6 @@ 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.
@@ -31,12 +23,10 @@ export function ControlPanel({ maintenanceMode }: ControlPanelProps) {
const handleAction = async (action: string, payload?: Record<string, unknown>) => {
setLoading(action);
try {
const token = window.AURORA_ENV?.ADMIN_TOKEN;
const response = await fetch(`/api/actions/${action}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
body: payload ? JSON.stringify(payload) : undefined,
});

View File

@@ -25,12 +25,7 @@ export function useActivityStats(): UseActivityStatsResult {
const fetchActivity = async () => {
setLoading(true);
try {
const token = (window as any).AURORA_ENV?.ADMIN_TOKEN;
const response = await fetch("/api/stats/activity", {
headers: {
"Authorization": `Bearer ${token}`
}
});
const response = await fetch("/api/stats/activity");
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const jsonData = await response.json();
setData(jsonData);

View File

@@ -22,6 +22,17 @@ interface DashboardStats {
totalWealth: string;
avgLevel: number;
topStreak: number;
totalItems: number;
};
activeLootdrops: Array<{
rewardAmount: number;
currency: string;
createdAt: string;
expiresAt: string | null;
}>;
leaderboards: {
topLevels: Array<{ username: string; level: number | null }>;
topWealth: Array<{ username: string; balance: string }>;
};
recentEvents: Array<{
type: 'success' | 'error' | 'info' | 'warn';

View File

@@ -5,7 +5,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Activity, Server, Users, Zap } from "lucide-react";
import { Activity, Server, Users, Zap, Package, Trophy, Coins } from "lucide-react";
import { useDashboardStats } from "@/hooks/use-dashboard-stats";
import { useActivityStats } from "@/hooks/use-activity-stats";
import { ControlPanel } from "@/components/ControlPanel";
@@ -62,10 +62,11 @@ export function Dashboard() {
<p className="text-white/40 font-medium">Monitoring real-time activity and core bot metrics.</p>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
{/* Metric Cards */}
{[
{ title: "Active Users", value: stats.users.active.toLocaleString(), label: `${stats.users.total.toLocaleString()} total registered`, icon: Users, color: "from-purple-500 to-pink-500" },
{ title: "Items in Circulation", value: stats.economy.totalItems.toLocaleString(), label: "Total items owned", icon: Package, color: "from-blue-500 to-cyan-500" },
{ title: "Commands registered", value: stats.commands.total, label: "Total system capabilities", icon: Zap, color: "from-yellow-500 to-orange-500" },
{ title: "Avg Latency", value: `${stats.ping.avg}ms`, label: "WebSocket heartbeat", icon: Activity, color: "from-emerald-500 to-teal-500" },
].map((metric, i) => (
@@ -144,6 +145,34 @@ export function Dashboard() {
{/* Administrative Control Panel */}
<ControlPanel maintenanceMode={stats.maintenanceMode} />
{/* Active Lootdrop Alert */}
{stats.activeLootdrops && stats.activeLootdrops.length > 0 && (
<Card className="bg-gradient-to-br from-red-500/10 to-orange-500/10 border-red-500/20 overflow-hidden relative">
<div className="absolute top-0 right-0 p-4 opacity-10">
<Zap className="w-24 h-24 text-red-500" />
</div>
<CardHeader className="pb-2">
<CardTitle className="text-lg font-bold text-red-400 flex items-center gap-2">
<span className="relative flex h-3 w-3">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
</span>
ACTIVE LOOTDROP
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-1">
<p className="text-2xl font-black text-white/90">
{stats.activeLootdrops[0]?.rewardAmount} {stats.activeLootdrops[0]?.currency}
</p>
<p className="text-xs font-medium text-white/50">
Expires <span className="text-red-300">{stats.activeLootdrops[0]?.expiresAt ? new Date(stats.activeLootdrops[0].expiresAt).toLocaleTimeString() : 'Never'}</span>
</p>
</div>
</CardContent>
</Card>
)}
{/* Recent Events Feed */}
<Card className="glass border-white/5 overflow-hidden flex-1">
<CardHeader className="bg-white/[0.02] border-b border-white/5">
@@ -187,6 +216,63 @@ export function Dashboard() {
</Card>
</div>
</div>
{/* Leaderboards Section */}
<div className="grid gap-6 md:grid-cols-2">
<Card className="glass border-white/5">
<CardHeader className="flex flex-row items-center gap-4">
<div className="p-2 bg-yellow-500/10 rounded-lg">
<Trophy className="w-5 h-5 text-yellow-500" />
</div>
<div>
<CardTitle className="text-lg font-bold">Top Levels</CardTitle>
<CardDescription className="text-white/40">Highest ranked users</CardDescription>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
{stats.leaderboards?.topLevels.map((user, i) => (
<div key={i} className="flex items-center justify-between p-3 rounded-lg bg-white/5 border border-white/5">
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm ${i === 0 ? 'bg-yellow-500/20 text-yellow-500' : i === 1 ? 'bg-gray-400/20 text-gray-400' : i === 2 ? 'bg-orange-700/20 text-orange-700' : 'bg-white/5 text-white/40'}`}>
{i + 1}
</div>
<span className="font-semibold">{user.username}</span>
</div>
<span className="font-mono text-sm text-white/60">Lvl {user.level}</span>
</div>
)) || <p className="text-white/20 text-sm">No data available</p>}
</div>
</CardContent>
</Card>
<Card className="glass border-white/5">
<CardHeader className="flex flex-row items-center gap-4">
<div className="p-2 bg-emerald-500/10 rounded-lg">
<Coins className="w-5 h-5 text-emerald-500" />
</div>
<div>
<CardTitle className="text-lg font-bold">Richest Users</CardTitle>
<CardDescription className="text-white/40">Highest net worth</CardDescription>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
{stats.leaderboards?.topWealth.map((user, i) => (
<div key={i} className="flex items-center justify-between p-3 rounded-lg bg-white/5 border border-white/5">
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm ${i === 0 ? 'bg-yellow-500/20 text-yellow-500' : i === 1 ? 'bg-gray-400/20 text-gray-400' : i === 2 ? 'bg-orange-700/20 text-orange-700' : 'bg-white/5 text-white/40'}`}>
{i + 1}
</div>
<span className="font-semibold">{user.username}</span>
</div>
<span className="font-mono text-sm text-white/60">{BigInt(user.balance).toLocaleString()} AU</span>
</div>
)) || <p className="text-white/20 text-sm">No data available</p>}
</div>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -17,6 +17,8 @@ mock.module("@shared/db/DrizzleClient", () => {
const mockBuilder = {
where: mock(() => Promise.resolve([{ count: "5", balance: 1000n, level: 5, dailyStreak: 2 }])),
then: (onfulfilled: any) => onfulfilled([{ count: "5", balance: 1000n, level: 5, dailyStreak: 2 }]),
orderBy: mock(() => mockBuilder), // Chainable
limit: mock(() => Promise.resolve([])), // Terminal
};
const mockFrom = {
@@ -33,6 +35,7 @@ mock.module("@shared/db/DrizzleClient", () => {
findFirst: mock(() => Promise.resolve({ username: "test" })),
findMany: mock(() => Promise.resolve([])),
},
lootdrops: { findMany: mock(() => Promise.resolve([])) },
}
},
};
@@ -100,37 +103,20 @@ describe("WebServer Security & Limits", () => {
expect(data.status).toBe("ok");
});
describe("Administrative Actions Authorization", () => {
test("should reject administrative actions without token", async () => {
describe("Administrative Actions", () => {
test("should allow administrative actions without token", async () => {
const response = await fetch(`http://localhost:${port}/api/actions/reload-commands`, {
method: "POST"
});
expect(response.status).toBe(401);
});
test("should reject administrative actions with invalid token", async () => {
const response = await fetch(`http://localhost:${port}/api/actions/reload-commands`, {
method: "POST",
headers: { "Authorization": "Bearer wrong-token" }
});
expect(response.status).toBe(401);
});
test("should accept administrative actions with valid token", async () => {
const { env } = await import("@shared/lib/env");
const response = await fetch(`http://localhost:${port}/api/actions/reload-commands`, {
method: "POST",
headers: { "Authorization": `Bearer ${env.ADMIN_TOKEN}` }
});
// Should be 200 (OK) or 500 (if underlying service fails, but NOT 401)
expect(response.status).not.toBe(401);
expect(response.status).toBe(200);
});
test("should reject maintenance mode with invalid payload", async () => {
const { env } = await import("@shared/lib/env");
const response = await fetch(`http://localhost:${port}/api/actions/maintenance-mode`, {
method: "POST",
headers: {
"Authorization": `Bearer ${env.ADMIN_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ not_enabled: true }) // Wrong field

View File

@@ -104,14 +104,6 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
if (url.pathname === "/api/stats/activity") {
try {
// Security Check: Token-based authentication
const { env } = await import("@shared/lib/env");
const authHeader = req.headers.get("Authorization");
if (authHeader !== `Bearer ${env.ADMIN_TOKEN}`) {
console.warn(`⚠️ [API] Unauthorized activity analytics access attempt from ${req.headers.get("x-forwarded-for") || "unknown"}`);
return new Response("Unauthorized", { status: 401 });
}
const now = Date.now();
// If we have a valid cache, return it
@@ -143,14 +135,6 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
// Administrative Actions
if (url.pathname.startsWith("/api/actions/") && req.method === "POST") {
try {
// Security Check: Token-based authentication
const { env } = await import("@shared/lib/env");
const authHeader = req.headers.get("Authorization");
if (authHeader !== `Bearer ${env.ADMIN_TOKEN}`) {
console.warn(`⚠️ [API] Unauthorized administrative action attempt from ${req.headers.get("x-forwarded-for") || "unknown"}`);
return new Response("Unauthorized", { status: 401 });
}
const { actionService } = await import("@shared/modules/admin/action.service");
const { MaintenanceModeSchema } = await import("@shared/modules/dashboard/dashboard.types");
@@ -201,10 +185,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
if (await fileRef.exists()) {
// If serving index.html, inject env vars for frontend
if (pathName === "/index.html") {
let html = await fileRef.text();
const { env } = await import("@shared/lib/env");
const envScript = `<script>window.AURORA_ENV = { ADMIN_TOKEN: "${env.ADMIN_TOKEN}" };</script>`;
html = html.replace("</head>", `${envScript}</head>`);
const html = await fileRef.text();
return new Response(html, { headers: { "Content-Type": "text/html" } });
}
return new Response(fileRef);
@@ -231,10 +212,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
return new Response("Dashboard Not Found", { status: 404 });
}
let indexHtml = await indexFile.text();
const { env: sharedEnv } = await import("@shared/lib/env");
const script = `<script>window.AURORA_ENV = { ADMIN_TOKEN: "${sharedEnv.ADMIN_TOKEN}" };</script>`;
indexHtml = indexHtml.replace("</head>", `${script}</head>`);
const indexHtml = await indexFile.text();
return new Response(indexHtml, { headers: { "Content-Type": "text/html" } });
},
@@ -312,15 +290,43 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service");
const { getClientStats } = await import("../../bot/lib/clientStats");
// Fetch all data in parallel
const [clientStats, activeUsers, totalUsers, economyStats, recentEvents] = await Promise.all([
// 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(),
]);
// 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;
console.error(`Failed to fetch ${name}:`, result.reason);
return defaultValue;
};
const clientStats = unwrap(results[0], {
bot: { name: 'Aurora', avatarUrl: null },
guilds: 0,
commandsRegistered: 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: [] }, 'leaderboards');
return {
bot: clientStats.bot,
guilds: { count: clientStats.guilds },
@@ -331,11 +337,20 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
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
})),
leaderboards,
uptime: clientStats.uptime,
lastCommandTimestamp: clientStats.lastCommandTimestamp,
maintenanceMode: (await import("../../bot/lib/BotClient")).AuroraClient.maintenanceMode,