feat(admin): server-safe StatusCard and Import hub polish
CI / check (push) Failing after 8s
CI / release (push) Skipped
CI / deploy (push) Skipped

Split OnlineUsersWidget from StatusCard, decouple ad delete button, sync badge import to ExternalTexts+WebsiteBadges, add Import section hub with cancelable SSE jobs and upload SQL option.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-08-01 15:48:21 +02:00
1 parent db957d7fb1
commit 9c4949186c
21 files changed
+576 -249

No files matched your search

+33
View File
@@ -34,6 +34,7 @@ import {
Terminal,
Ticket,
Trophy,
Upload,
Users,
UsersRound,
Volume2,
@@ -217,6 +218,38 @@ export const ADMIN_HUBS: AdminHubDefinition[] = [
{ href: "/admin/devops/errors", labelKey: "errors" },
],
},
{
id: "import",
titleKey: "import",
subtitleKey: "importSubtitle",
icon: Upload,
prefixes: ["/admin/import"],
tabs: [
{
href: "/admin/import",
labelKey: "importOverview",
match: ["/admin/import"],
},
{ href: "/admin/import/badges", labelKey: "importBadges" },
{
href: "/admin/import/furni/upload",
labelKey: "importUpload",
match: ["/admin/import/furni/upload"],
},
{
href: "/admin/import/furni",
labelKey: "importFurni",
match: ["/admin/import/furni"],
},
{ href: "/admin/import/clothing", labelKey: "importClothing" },
{ href: "/admin/import/effects", labelKey: "importEffects" },
{ href: "/admin/import/pets", labelKey: "importPets" },
{ href: "/admin/import/clone", labelKey: "importClone" },
{ href: "/admin/import/sync", labelKey: "importSync" },
{ href: "/admin/import/repair-icons", labelKey: "importRepairIcons" },
{ href: "/admin/import/audit", labelKey: "importAudit" },
],
},
];
/** Longest-prefix match so nested routes (e.g. /admin/logs/audit) win. */
+17
View File
@@ -2,11 +2,13 @@ import { existsSync, readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const ROUTES = [
"audit",
"badges",
"badges/edit",
"clone",
"clone/batch",
"clone/icon",
"clone/sync-all",
"clothing",
"clothing/batch",
"clothing/sets",
@@ -17,7 +19,9 @@ const ROUTES = [
"furni/batch",
"furni/batch-regen",
"furni/nitro-editor",
"furni/repair-icons",
"furni/resync",
"furni/upload",
"pets",
"pets/batch",
"pets/icon",
@@ -35,4 +39,17 @@ describe("admin import backend contract", () => {
const source = readFileSync("src/actions/import-furni.ts", "utf8");
expect(source).not.toContain("deleted: 0, remaining: 0");
});
it("keeps badge import ExternalTexts + WebsiteBadges in sync", () => {
const service = readFileSync("src/lib/services/import-badge.ts", "utf8");
expect(service).toContain("ExternalTexts.json");
expect(service).toContain("WebsiteBadges");
expect(service).toContain("importBadgeSynced");
expect(readFileSync("src/actions/import-badges.ts", "utf8")).toContain(
"importBadgeSynced",
);
expect(
readFileSync("src/app/api/admin/import/badges/route.ts", "utf8"),
).toContain("importBadgeSynced");
});
});
+105
View File
@@ -0,0 +1,105 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { db, WebsiteBadges } from "@/lib/db";
const EXTERNAL_TEXTS_PATH = path.join(
process.cwd(),
"public/nitro-assets/gamedata/ExternalTexts.json",
);
export type ImportBadgeResult =
| { ok: true }
| { ok: false; error: string; status: number };
/** Write badge name/desc into ExternalTexts.json (409 if already present). */
export async function writeBadgeToExternalTexts(
code: string,
name: string,
description: string,
): Promise<ImportBadgeResult> {
try {
const raw = await fs.readFile(EXTERNAL_TEXTS_PATH, "utf-8");
const texts = JSON.parse(raw) as Record<string, string>;
const nameKey = `badge_name_${code}`;
const descKey = `badge_desc_${code}`;
if (texts[nameKey] !== undefined) {
return {
ok: false,
error: "Badge already exists in ExternalTexts.json",
status: 409,
};
}
texts[nameKey] = name;
texts[descKey] = description || "";
await fs.writeFile(
EXTERNAL_TEXTS_PATH,
JSON.stringify(texts, null, 4),
"utf-8",
);
return { ok: true };
} catch (err) {
console.warn(
"[import-badges] Failed to update ExternalTexts.json:",
(err as Error).message,
);
return {
ok: false,
error: "Failed to update ExternalTexts.json",
status: 500,
};
}
}
/** Upsert WebsiteBadges row so CMS badge tables stay in sync. */
export async function upsertWebsiteBadge(
code: string,
name: string,
description: string,
): Promise<void> {
const now = new Date();
await db
.insert(WebsiteBadges)
.values({
badgeKey: code,
badgeName: name,
badgeDescription: description,
createdAt: now,
updatedAt: now,
})
.onDuplicateKeyUpdate({
set: {
badgeName: name,
badgeDescription: description,
updatedAt: now,
},
});
}
/**
* Single import path: ExternalTexts.json + WebsiteBadges.
* Callers should gate with ASSETS_IMPORT.
*/
export async function importBadgeSynced(input: {
code: string;
name: string;
description: string;
}): Promise<ImportBadgeResult> {
const texts = await writeBadgeToExternalTexts(
input.code,
input.name,
input.description,
);
if (!texts.ok) return texts;
try {
await upsertWebsiteBadge(input.code, input.name, input.description);
} catch {
return { ok: false, error: "Failed to import badge", status: 500 };
}
return { ok: true };
}
+30
View File
@@ -179,6 +179,21 @@ describe("staff smoke contract", () => {
);
});
it("keeps StatusCard server-safe without client boundary", () => {
expect(
readFileSync("src/components/admin/dashboard.tsx", "utf8"),
).not.toMatch(/^["']use client["']/m);
expect(existsSync("src/components/admin/online-users-widget.tsx")).toBe(
true,
);
expect(
readFileSync("src/components/admin/online-users-widget.tsx", "utf8"),
).toContain("use client");
expect(existsSync("src/app/admin/ads/edit-ad-delete-button.tsx")).toBe(
true,
);
});
it("ships ops health alerts, optional DB backup, and health rate limit", () => {
const worker = readFileSync("scripts/jobs-worker.ts", "utf8");
expect(worker).toContain("checkOpsHealth");
@@ -215,4 +230,19 @@ describe("staff smoke contract", () => {
"/admin/items",
);
});
it("ships import hub with synced badge import path", () => {
const nav = readFileSync("src/lib/admin-nav.ts", "utf8");
expect(nav).toContain('id: "import"');
expect(nav).toContain("importSubtitle");
expect(nav).toContain("/admin/import");
expect(existsSync("src/lib/services/import-badge.ts")).toBe(true);
const badgeService = readFileSync(
"src/lib/services/import-badge.ts",
"utf8",
);
expect(badgeService).toContain("ExternalTexts.json");
expect(badgeService).toContain("WebsiteBadges");
expect(badgeService).toContain("importBadgeSynced");
});
});