refactor: remove PageTransition component and adjust TierlistSelectionPage layout

feat: add card caching and database seeding functionality

chore: update biome schema version and adjust linting rules

fix: correct table reference handling in cdnReferences view

feat: implement card API endpoint for fetching and caching character cards

chore: update dependencies and remove unused framer-motion package

chore: enhance Docker setup with health checks and improved service definitions

fix: ensure proper handling of admin account creation during database seeding

style: enforce stricter TypeScript checks and improve code readability
This commit is contained in:
2026-02-02 00:10:25 +07:00 Unverified
parent f086b9aaff
commit 6104b420c0
26 changed files with 823 additions and 592 deletions
+196 -23
View File
@@ -1,19 +1,25 @@
import { env } from "node:process";
import { formatDistanceToNow } from "date-fns";
import { not } from "drizzle-orm";
import { and, asc, eq, isNull, lt, not, sql } from "drizzle-orm";
import { removeExpiredSubmissions } from "@/app/(ui)/rubgram/api";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { artifactSettings, endgameSubmissions } from "@/lib/db/schema";
Bun.serve({
fetch(req, server) {
return new Response("Hello world2");
},
});
console.log("Listening");
import {
artifactSettings,
cards,
endgameSettings,
endgameSubmissions,
endgameTypes,
settings,
submissions,
tierlistColumns,
tierlistTiers,
user,
} from "@/lib/db/schema";
function logger(group: string) {
const prefix = `[${group}]`;
return {
const t = {
// biome-ignore lint/suspicious/noExplicitAny: logger
log(...args: any[]) {
console.log(prefix, ...args);
@@ -22,7 +28,7 @@ function logger(group: string) {
error(...args: any[]) {
console.error(prefix, ...args);
},
schedule(target: Date[] | number, callback: () => void) {
schedule(target: Date[] | number, callback: () => Promise<void> | void) {
let timeout: number;
const now = Date.now();
// conv sec to milli
@@ -34,17 +40,19 @@ function logger(group: string) {
.map((d) => d.getTime() - now)
.reduce((a, b) => Math.min(a, b), 0)
: 60000;
console.log(
prefix,
`Running ${formatDistanceToNow(new Date(now + timeout), { addSuffix: true })}`,
);
return setTimeout(callback, timeout);
if (typeof target === "number" || target.length)
console.log(
prefix,
`Running ${formatDistanceToNow(new Date(now + timeout), { addSuffix: true })}`,
);
return setTimeout(() => callback()?.catch(t.error), timeout);
},
};
return t;
}
async function checkRubgramExpiration() {
const { log, error, schedule } = logger("rubgramExpiration");
const { log, schedule } = logger("rubgramExpiration");
const { removed } = await removeExpiredSubmissions();
if (removed) log("Purged", removed, "submissions");
@@ -58,15 +66,180 @@ async function checkRubgramExpiration() {
);
}
async function mockData() {
if (process.env.NODE_ENV !== "development") return;
const { log, error, schedule } = logger("mockData");
const res = await db.select().from(artifactSettings);
if (res.length) return;
// schema isn't there yet
async function seedDatabase() {
if (process.env.ENVIRONMENT !== "development") return;
const { log, error } = logger("seeding");
try {
const res = await db.select().from(artifactSettings);
if (res.length) return;
} catch {
// schema isn't there yet
error("Schema is not applied or not updated, skipping database seeding");
return;
}
log("Setting up database schema");
const existing = await db
.select({ id: user.id })
.from(user)
.where(eq(user.email, "[email protected]"));
if (existing) log("Admin account already exist, skipping creation");
else if (env.INITIAL_ADMIN_PWD) {
const u = await auth.api.signUpEmail({
body: {
name: "Admin",
email: "[email protected]",
password: env.INITIAL_ADMIN_PWD,
},
});
await db
.update(user)
.set({
role: "admin",
})
.where(eq(user.id, u.user.id));
log(
"User [email protected] created successfully with INITIAL_ADMIN_PWD password",
);
} else
return error("INITIAL_ADMIN_PWD not specified, skipping database seeding");
log("Transaction BEGIN");
await db.transaction(async (tx) => {
log("Creating default Global Settings");
await tx.insert(settings).values({
id: true,
enka: true,
});
log("Creating default Artifact Settings");
await tx.insert(artifactSettings).values({
id: true,
limit: -1,
locked: true,
});
log("Creating default Endgame Settings");
await tx.insert(endgameSettings).values({
id: true,
limit: -1,
locked: true,
allDiscount: 10,
free: 2,
});
await tx.insert(endgameTypes).values([
{ id: "abyss", display: "Spiral Abyss (อบิส)", price: 60, order: 1 },
{ id: "theater", display: "โรงละครในจินตนาการ", price: 100, order: 2 },
{
id: "stygian",
display: "Stygian Onslaught (ศึกเดือดแดนเร้นลับ)",
price: 100,
order: 3,
},
{
id: "stygian1rm",
display: "ลง 1 ห้อง (Stygian Onslaught)",
price: 35,
order: 4,
},
]);
log("Creating default Tierlist layout");
await tx.insert(tierlistTiers).values([
{
id: "p",
name: "SS",
order: 10,
badges: ["sss", "ssp", "ssm"],
},
{
id: "s",
name: "S",
order: 20,
badges: ["sp", "sm"],
},
{
id: "a",
name: "A",
order: 30,
badges: ["ap", "am"],
},
{
id: "b",
name: "B",
order: 40,
badges: ["bp", "bm"],
},
{
id: "c",
name: "C",
order: 50,
badges: ["cp", "cm"],
},
]);
await tx.insert(tierlistColumns).values([
{
id: "nd",
name: "On-Field DPS",
order: 10,
},
{
id: "fd",
name: "Off-Field DPS",
order: 20,
},
{
id: "s",
name: "Support",
order: 30,
},
]);
});
log("Database seeding complete");
}
async function cacheCards() {
const { log, schedule } = logger("cacheCards");
const [notCached] = await db
.select({
id: submissions.id,
})
.from(submissions)
.leftJoin(cards, eq(cards.submission, submissions.id))
.where(
and(
eq(submissions.checked, false),
isNull(cards.image),
lt(sql<number>`coalesce(${cards.tries}, 0)`, 20),
),
)
.orderBy(
asc(submissions.queue),
asc(sql`coalesce(${cards.tries}, 0)`),
asc(submissions.id),
)
.limit(1);
if (!notCached) {
schedule([], cacheCards);
return;
}
log(`Generating card for ${notCached.id}`);
const res = await fetch(`http://app:3000/api/card/${notCached.id}`);
if (!res.ok) {
log(`Wasn't okay, queueing for retry.`);
await db
.insert(cards)
.values({
submission: notCached.id,
tries: 1,
})
.onConflictDoUpdate({
target: cards.submission,
set: {
tries: res.status === 400 ? 20 : sql<number>`${cards.tries} + 1`,
},
});
schedule(60, cacheCards);
} else schedule(120, cacheCards);
}
checkRubgramExpiration();
seedDatabase();
cacheCards();
process.on(15 as unknown as "SIGTERM", process.exit);