diff --git a/src/app/(site)/apply/team/page.tsx b/src/app/(site)/apply/team/page.tsx
index 5dfb038a..a07f8aaf 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 25e790c6..7f4aacd0 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 60474b37..e9b7f0e1 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 663a7947..f9045d2a 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 28a28d89..50f4d9a7 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 b10288d3..918f2446 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 65e150e3..2906626a 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 6ac5a3a9..37f12b28 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 5ac0420b..1786fce3 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 e939fe24..15fc9f64 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 5afa0f87..d93185b3 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 c44292ad..8bdd6d98 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 d56eb08a..2e406bd9 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 46133704..4be15a12 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 18a2585e..edb6cdef 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",