diff --git a/src/app/(site)/apply/team/page.tsx b/src/app/(site)/apply/team/page.tsx index 5dfb038af6..a07f8aaf9f 100644 --- a/src/app/(site)/apply/team/page.tsx +++ b/src/app/(site)/apply/team/page.tsx @@ -57,7 +57,7 @@ export default async function ApplyTeamPage({ const hotelName = await resolveHotelName(); // ── Teams open for application ────────────────────────────── - // The Prisma slice exposes teams directly (no separate team-position table + // Teams are exposed directly (no separate team-position table // with position_kind), so the team application concept lists website_teams. // Hidden ranks are excluded from the public apply page. const teams = await db diff --git a/src/app/(site)/radio/page.tsx b/src/app/(site)/radio/page.tsx index 25e790c6fa..7f4aacd00a 100644 --- a/src/app/(site)/radio/page.tsx +++ b/src/app/(site)/radio/page.tsx @@ -21,7 +21,7 @@ const WEEKDAYS = [ ]; // radio_schedules.start_time / end_time are stored as TIME and surface through -// Prisma as a Date with the time component on a 1970 epoch date — render just +// as a Date with the time component on a 1970 epoch date — render just // the HH:MM portion in UTC to avoid timezone drift. function formatTime(d: Date | null | undefined): string { if (!d) return ""; diff --git a/src/app/(site)/radio/schedule/page.tsx b/src/app/(site)/radio/schedule/page.tsx index 60474b37ca..e9b7f0e118 100644 --- a/src/app/(site)/radio/schedule/page.tsx +++ b/src/app/(site)/radio/schedule/page.tsx @@ -17,7 +17,7 @@ const DAYS = [ "Sunday", ] as const; -// `start_time` / `end_time` are MySQL TIME columns; Prisma hydrates them as a +// `start_time` / `end_time` are MySQL TIME columns; the driver hydrates them as a // Date on the 1970-01-01 epoch. Pull just HH:MM in UTC (Time has no zone). function formatTime(d: Date | null | undefined): string { if (!d) return ""; diff --git a/src/app/admin/help-tickets/[id]/page.tsx b/src/app/admin/help-tickets/[id]/page.tsx index 663a7947fa..f9045d2ab7 100644 --- a/src/app/admin/help-tickets/[id]/page.tsx +++ b/src/app/admin/help-tickets/[id]/page.tsx @@ -3,6 +3,7 @@ import { notFound, redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner"; import { getMinStaffRank } from "@/lib/admin/min-staff-rank"; +import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts"; import { positiveBigInt } from "@/lib/api"; import { unixNow } from "@/lib/bans"; import { @@ -141,6 +142,7 @@ export default async function AdminHelpTicketDetailPage({ const isBanAppeal = /ban\s*appeal/i.test(ticket.title) || /ban\s*appeal/i.test(category?.name ?? ""); + const queues = await fetchTicketQueueOpenCounts(); return (
@@ -148,6 +150,10 @@ export default async function AdminHelpTicketDetailPage({ hint={t("queueHint")} otherHref="/admin/tickets" otherLabel={t("queueOtherCta")} + countsLine={t("queueCounts", { + here: queues.helpOpen, + other: queues.cmsOpen, + })} /> +

+ {t("purgeHint")} +

@@ -66,6 +68,10 @@ export default async function AdminTicketDetailPage({ hint={t("queueHint")} otherHref="/admin/help-tickets" otherLabel={t("queueOtherCta")} + countsLine={t("queueCounts", { + here: queues.cmsOpen, + other: queues.helpOpen, + })} /> +
+ + +
); } diff --git a/src/app/mod/tickets/[id]/page.tsx b/src/app/mod/tickets/[id]/page.tsx index 28a28d895c..50f4d9a7cf 100644 --- a/src/app/mod/tickets/[id]/page.tsx +++ b/src/app/mod/tickets/[id]/page.tsx @@ -1,9 +1,12 @@ import { asc, eq, gte } from "drizzle-orm"; import { alias } from "drizzle-orm/mysql-core"; import { notFound } from "next/navigation"; +import { getTranslations } from "next-intl/server"; import { AdminTicketDetail } from "@/app/admin/tickets/[id]/admin-ticket-detail"; +import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner"; import { requireModPermission } from "@/lib/admin/guard"; import { getMinStaffRank } from "@/lib/admin/min-staff-rank"; +import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts"; import { db, User, WebsiteTicket, WebsiteTicketMessage } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; @@ -77,47 +80,61 @@ export default async function ModTicketDetailPage({ canAccess(permissions, PERMS.MOD_TICKETS_EDIT, session.user.rank) || canAccess(permissions, PERMS.TICKETS_EDIT, session.user.rank); + const t = await getTranslations("pages.admin.tickets"); + const queues = await fetchTicketQueueOpenCounts(); + return ( - ({ - id: m.id, - message: m.message, - isStaff: m.isStaff === 1, - createdAt: m.createdAt.toISOString(), - user: { - id: m.userId, - username: m.username, - look: m.look, - rank: m.rank, - }, - }))} - staffMembers={staffMembers} - currentUserId={session.user.id} - canEdit={canEdit} - listHref="/mod/tickets" - userLinkMode="public" - /> +
+ + ({ + id: m.id, + message: m.message, + isStaff: m.isStaff === 1, + createdAt: m.createdAt.toISOString(), + user: { + id: m.userId, + username: m.username, + look: m.look, + rank: m.rank, + }, + }))} + staffMembers={staffMembers} + currentUserId={session.user.id} + canEdit={canEdit} + listHref="/mod/tickets" + userLinkMode="public" + /> +
); } diff --git a/src/db/schema.ts b/src/db/schema.ts index b10288d32d..918f24461a 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -24,7 +24,7 @@ import { varchar, } from "drizzle-orm/mysql-core"; -// MySQL TIME / DATE columns are hydrated by Prisma as JS Date (epoch 1970-01-01 +// MySQL TIME / DATE columns are hydrated as JS Date (epoch 1970-01-01 // for TIME). Mirror that so existing call sites keep working unchanged. const timeAsDate = customType<{ data: Date; driverData: string }>({ dataType() { diff --git a/src/lib/api.test.ts b/src/lib/api.test.ts index 65e150e390..2906626ae4 100644 --- a/src/lib/api.test.ts +++ b/src/lib/api.test.ts @@ -27,7 +27,7 @@ describe("positiveBigInt", () => { }); describe("pagination", () => { - it("rejects fractional and malformed values before they reach Prisma", () => { + it("rejects fractional and malformed values before they reach the DB layer", () => { const params = new URLSearchParams({ page: "2.5", perPage: "10.1" }); expect(pagination(params, 20, 100)).toEqual({ diff --git a/src/lib/api.ts b/src/lib/api.ts index 6ac5a3a910..37f12b28a1 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; /** - * JSON response helper for the public REST API. Serialises BigInt (Prisma ids) + * JSON response helper for the public REST API. Serialises BigInt (DB ids) * to strings — JSON.stringify throws on BigInt otherwise — and sets CORS to * APP_URL so the game client / external integrations can read it. */ diff --git a/src/lib/bans.ts b/src/lib/bans.ts index 5ac0420bc0..1786fce3a6 100644 --- a/src/lib/bans.ts +++ b/src/lib/bans.ts @@ -14,7 +14,7 @@ export function isBanActive( return banExpire === 0 || banExpire > now; } -/** Prisma `where` fragment for currently active bans (incl. permanent). */ +/** Filter for currently active bans (incl. permanent). */ export function activeBanWhere(now: number = unixNow()) { return { OR: [{ banExpire: 0 }, { banExpire: { gt: now } }], diff --git a/src/lib/proxy-auth.ts b/src/lib/proxy-auth.ts index e939fe2494..15fc9f6426 100644 --- a/src/lib/proxy-auth.ts +++ b/src/lib/proxy-auth.ts @@ -2,7 +2,7 @@ import NextAuth from "next-auth"; import { env } from "@/env"; // Proxy authentication must only decode the Auth.js session. Importing the -// full CMS auth configuration here would also run Prisma/settings callbacks. +// full CMS auth configuration here would also run DB/settings callbacks. export const { auth: proxyAuth } = NextAuth({ trustHost: true, secret: env.AUTH_SECRET, diff --git a/src/lib/services/catalog-items-loader.ts b/src/lib/services/catalog-items-loader.ts index 5afa0f87ad..d93185b368 100644 --- a/src/lib/services/catalog-items-loader.ts +++ b/src/lib/services/catalog-items-loader.ts @@ -138,7 +138,7 @@ export async function loadCatalogItemsData( ): Promise { // Load items via raw query to work around pageId Int vs VARCHAR mismatch const pageIdStr = String(pageId); - // CAST: live Habbo DBs often store page_id as VARCHAR while Prisma maps Int. + // CAST: live Habbo DBs often store page_id as VARCHAR while schema maps Int. const [rawItems] = (await db.execute(sql` SELECT * FROM catalog_items WHERE CAST(page_id AS CHAR) = ${pageIdStr} diff --git a/src/lib/services/catalog-tree.ts b/src/lib/services/catalog-tree.ts index c44292ad45..8bdd6d98cb 100644 --- a/src/lib/services/catalog-tree.ts +++ b/src/lib/services/catalog-tree.ts @@ -12,7 +12,7 @@ function toInt(value: unknown, fallback = 0): number { /** * Count catalog_items per page via raw SQL. - * Real Habbo DBs often store page_id as VARCHAR; Prisma Int groupBy fails or returns 0. + * Real Habbo DBs often store page_id as VARCHAR; numeric groupBy fails or returns 0. */ export async function getCatalogItemCounts( pageIds?: number[], @@ -216,7 +216,7 @@ export async function movePage( /** * Delete catalog_items for the given page ids. - * Habbo DBs often store page_id as VARCHAR; Prisma Int deleteMany misses rows. + * Habbo DBs often store page_id as VARCHAR; typed Int deletes can miss rows. */ async function deleteCatalogItemsByPageIds(pageIds: number[]): Promise { if (pageIds.length === 0) return; diff --git a/src/lib/staff-smoke-contract.test.ts b/src/lib/staff-smoke-contract.test.ts index d56eb08a1e..2e406bd94a 100644 --- a/src/lib/staff-smoke-contract.test.ts +++ b/src/lib/staff-smoke-contract.test.ts @@ -55,10 +55,13 @@ describe("staff smoke contract", () => { expect(readFileSync("scripts/apply-migrations.ts", "utf8")).toContain( "drizzle/migrations", ); - expect(readFileSync("package.json", "utf8")).not.toContain('"prisma"'); - expect(readFileSync("package.json", "utf8")).not.toContain( - '"@prisma/client"', - ); + const pkg = readFileSync("package.json", "utf8"); + expect(pkg).not.toContain('"prisma"'); + expect(pkg).not.toContain('"@prisma/client"'); + expect(pkg).not.toContain("prisma:generate"); + expect(pkg).toContain('"db:generate": "drizzle-kit generate"'); + expect(pkg).toContain('"db:studio": "drizzle-kit studio"'); + expect(pkg).toContain('"db:introspect": "drizzle-kit introspect"'); expect(readFileSync("drizzle.config.ts", "utf8")).toContain( "./drizzle/drafts", ); @@ -144,9 +147,16 @@ describe("staff smoke contract", () => { "src/app/admin/help-tickets/page.tsx", "src/app/mod/tickets/desk/page.tsx", "src/app/mod/help-tickets/page.tsx", + "src/app/admin/tickets/[id]/page.tsx", + "src/app/admin/help-tickets/[id]/page.tsx", + "src/app/mod/tickets/[id]/page.tsx", + "src/app/mod/help-tickets/[id]/page.tsx", ]) { expect(existsSync(path), path).toBe(true); expect(readFileSync(path, "utf8"), path).toContain("TicketQueueBanner"); + expect(readFileSync(path, "utf8"), path).toContain( + "fetchTicketQueueOpenCounts", + ); } }); @@ -169,7 +179,16 @@ describe("staff smoke contract", () => { ); }); - it("ships items_base browser", () => { + it("documents local-only photo file purge", () => { + expect(readFileSync("src/app/admin/photos/page.tsx", "utf8")).toContain( + "purgeHint", + ); + expect(readFileSync("src/messages/en.json", "utf8")).toContain( + "External CDN URLs are not purged", + ); + }); + + it("ships items base admin CRUD", () => { expect(existsSync("src/app/admin/items/page.tsx")).toBe(true); expect(existsSync("src/app/admin/items/[id]/page.tsx")).toBe(true); expect(readFileSync("src/actions/items-base.ts", "utf8")).toContain( diff --git a/src/messages/en.json b/src/messages/en.json index 46133704cf..4be15a129a 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -1351,6 +1351,7 @@ "photos": { "title": "Photo Moderation", "subtitle": "Recent in-game camera photos", + "purgeHint": "Delete removes the database row and best-effort deletes a matching file under /public. External CDN URLs are not purged.", "delete": "Delete", "approve": "Approve", "noPhotos": "No photos to moderate", diff --git a/src/messages/it.json b/src/messages/it.json index 18a2585e49..edb6cdefb4 100644 --- a/src/messages/it.json +++ b/src/messages/it.json @@ -1326,6 +1326,7 @@ "photos": { "title": "Moderazione foto", "subtitle": "Foto recenti dalla fotocamera di gioco", + "purgeHint": "L'eliminazione rimuove la riga DB e, se possibile, il file locale sotto /public. Gli URL CDN esterni non vengono purgati.", "delete": "Elimina", "approve": "Approva", "noPhotos": "Nessuna foto da moderare",