feat(admin): server-safe StatusCard and Import hub polish
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:
1 parent
db957d7fb1
commit
9c4949186c
21 files changed
+576
-249
No files matched your search
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user