feat(portal): add SSR operations and exclusive groups
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { fillDailySeries } from "./admin-metrics";
|
||||
|
||||
describe("admin dashboard metrics", () => {
|
||||
it("fills missing UTC registration days with zero", () => {
|
||||
expect(fillDailySeries(
|
||||
[{ day: "2026-07-30", count: 2 }, { day: "2026-08-01", count: 1 }],
|
||||
new Date("2026-08-01T22:00:00Z"),
|
||||
3,
|
||||
)).toEqual([
|
||||
{ day: "2026-07-30", count: 2 },
|
||||
{ day: "2026-07-31", count: 0 },
|
||||
{ day: "2026-08-01", count: 1 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface DailyCount {
|
||||
day: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export function fillDailySeries(rows: DailyCount[], end: Date, days: number) {
|
||||
const counts = new Map(rows.map((row) => [row.day, Number(row.count)]));
|
||||
const endDay = new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate()));
|
||||
return Array.from({ length: days }, (_, index) => {
|
||||
const date = new Date(endDay);
|
||||
date.setUTCDate(endDay.getUTCDate() - (days - index - 1));
|
||||
const day = date.toISOString().slice(0, 10);
|
||||
return { day, count: counts.get(day) ?? 0 };
|
||||
});
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { hasDiscordNicknameConfirmation } from "./dashboard-change-confirmation";
|
||||
|
||||
describe("dashboard identity change confirmation", () => {
|
||||
it("accepts only the explicit Discord nickname confirmation value", () => {
|
||||
expect(hasDiscordNicknameConfirmation(new FormData())).toBe(false);
|
||||
|
||||
const declined = new FormData();
|
||||
declined.set("confirmDiscordNickname", "no");
|
||||
expect(hasDiscordNicknameConfirmation(declined)).toBe(false);
|
||||
|
||||
const confirmed = new FormData();
|
||||
confirmed.set("confirmDiscordNickname", "yes");
|
||||
expect(hasDiscordNicknameConfirmation(confirmed)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export function hasDiscordNicknameConfirmation(formData: FormData) {
|
||||
return formData.get("confirmDiscordNickname") === "yes";
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { eventCategory, normalizeSelectedEventTypes } from "./event-filters";
|
||||
|
||||
describe("event filters", () => {
|
||||
it("accepts only available event types and removes duplicates", () => {
|
||||
expect(normalizeSelectedEventTypes(["login.allowed", "unknown", "login.allowed"], ["login.allowed", "group.updated"]))
|
||||
.toEqual(["login.allowed"]);
|
||||
});
|
||||
|
||||
it("classifies events into operator-friendly views", () => {
|
||||
expect(eventCategory("games.minecraft.account-manager.group.deleted")).toBe("groups");
|
||||
expect(eventCategory("games.minecraft.account-manager.game.login.denied")).toBe("admission");
|
||||
expect(eventCategory("games.minecraft.account-manager.network.vpn-blocked")).toBe("security");
|
||||
expect(eventCategory("games.minecraft.account-manager.auth.magic-link.consumed")).toBe("security");
|
||||
expect(eventCategory("games.minecraft.account-manager.discord.nickname.updated")).toBe("identity");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
export const eventCategoryValues = ["all", "admission", "security", "identity", "groups", "operations"] as const;
|
||||
export type EventCategory = (typeof eventCategoryValues)[number];
|
||||
|
||||
export function eventCategory(type: string): Exclude<EventCategory, "all"> {
|
||||
if (type.includes(".group.")) return "groups";
|
||||
if (type.includes(".network.") || type.includes(".auth.") || type.includes("authentication") || type.includes("replay")) return "security";
|
||||
if (type.includes(".game.login.")) return "admission";
|
||||
if (type.includes(".discord.") || type.includes(".user.") || type.includes("minecraft-account")) return "identity";
|
||||
return "operations";
|
||||
}
|
||||
|
||||
export function normalizeEventCategory(value: string | undefined): EventCategory {
|
||||
return eventCategoryValues.includes(value as EventCategory) ? value as EventCategory : "all";
|
||||
}
|
||||
|
||||
export function normalizeSelectedEventTypes(value: string | string[] | undefined, availableTypes: string[]) {
|
||||
const requested = Array.isArray(value) ? value : value ? [value] : [];
|
||||
const available = new Set(availableTypes);
|
||||
return [...new Set(requested.filter((type) => available.has(type)))].slice(0, 20);
|
||||
}
|
||||
Reference in New Issue
Block a user