fix(admin): polish tickets, photos purge note, drizzle contracts
Add queue banners/counts on ticket detail pages, document local-only photo purge, and harden Drizzle Kit smoke contracts after Prisma removal. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
ba82789166
commit
3bd712e744
18 files changed
+148
-78
No files matched your search
@@ -57,7 +57,7 @@ export default async function ApplyTeamPage({
|
|||||||
const hotelName = await resolveHotelName();
|
const hotelName = await resolveHotelName();
|
||||||
|
|
||||||
// ── Teams open for application ──────────────────────────────
|
// ── 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.
|
// with position_kind), so the team application concept lists website_teams.
|
||||||
// Hidden ranks are excluded from the public apply page.
|
// Hidden ranks are excluded from the public apply page.
|
||||||
const teams = await db
|
const teams = await db
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const WEEKDAYS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// radio_schedules.start_time / end_time are stored as TIME and surface through
|
// 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.
|
// the HH:MM portion in UTC to avoid timezone drift.
|
||||||
function formatTime(d: Date | null | undefined): string {
|
function formatTime(d: Date | null | undefined): string {
|
||||||
if (!d) return "";
|
if (!d) return "";
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const DAYS = [
|
|||||||
"Sunday",
|
"Sunday",
|
||||||
] as const;
|
] 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).
|
// Date on the 1970-01-01 epoch. Pull just HH:MM in UTC (Time has no zone).
|
||||||
function formatTime(d: Date | null | undefined): string {
|
function formatTime(d: Date | null | undefined): string {
|
||||||
if (!d) return "";
|
if (!d) return "";
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { notFound, redirect } from "next/navigation";
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||||
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
||||||
|
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
|
||||||
import { positiveBigInt } from "@/lib/api";
|
import { positiveBigInt } from "@/lib/api";
|
||||||
import { unixNow } from "@/lib/bans";
|
import { unixNow } from "@/lib/bans";
|
||||||
import {
|
import {
|
||||||
@@ -141,6 +142,7 @@ export default async function AdminHelpTicketDetailPage({
|
|||||||
const isBanAppeal =
|
const isBanAppeal =
|
||||||
/ban\s*appeal/i.test(ticket.title) ||
|
/ban\s*appeal/i.test(ticket.title) ||
|
||||||
/ban\s*appeal/i.test(category?.name ?? "");
|
/ban\s*appeal/i.test(category?.name ?? "");
|
||||||
|
const queues = await fetchTicketQueueOpenCounts();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -148,6 +150,10 @@ export default async function AdminHelpTicketDetailPage({
|
|||||||
hint={t("queueHint")}
|
hint={t("queueHint")}
|
||||||
otherHref="/admin/tickets"
|
otherHref="/admin/tickets"
|
||||||
otherLabel={t("queueOtherCta")}
|
otherLabel={t("queueOtherCta")}
|
||||||
|
countsLine={t("queueCounts", {
|
||||||
|
here: queues.helpOpen,
|
||||||
|
other: queues.cmsOpen,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
<AdminHelpTicketDetail
|
<AdminHelpTicketDetail
|
||||||
ticket={{
|
ticket={{
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ export default async function AdminPhotos({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminPageShell icon={Camera} title={t("title")} subtitle={t("subtitle")}>
|
<AdminPageShell icon={Camera} title={t("title")} subtitle={t("subtitle")}>
|
||||||
|
<p className="m-0 mb-4 text-sm text-[var(--admin-text-muted)]">
|
||||||
|
{t("purgeHint")}
|
||||||
|
</p>
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||||
<StatusCard label={t("photosShown")} value={photos.length} icon="📸" />
|
<StatusCard label={t("photosShown")} value={photos.length} icon="📸" />
|
||||||
<StatusCard
|
<StatusCard
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { notFound, redirect } from "next/navigation";
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||||
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
||||||
|
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
|
||||||
import { db, User, WebsiteTicket } from "@/lib/db";
|
import { db, User, WebsiteTicket } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { AdminTicketDetail } from "./admin-ticket-detail";
|
import { AdminTicketDetail } from "./admin-ticket-detail";
|
||||||
@@ -59,6 +60,7 @@ export default async function AdminTicketDetailPage({
|
|||||||
.orderBy(asc(User.username));
|
.orderBy(asc(User.username));
|
||||||
|
|
||||||
const canEdit = canAccess(permissions, PERMS.TICKETS_EDIT, session.user.rank);
|
const canEdit = canAccess(permissions, PERMS.TICKETS_EDIT, session.user.rank);
|
||||||
|
const queues = await fetchTicketQueueOpenCounts();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -66,6 +68,10 @@ export default async function AdminTicketDetailPage({
|
|||||||
hint={t("queueHint")}
|
hint={t("queueHint")}
|
||||||
otherHref="/admin/help-tickets"
|
otherHref="/admin/help-tickets"
|
||||||
otherLabel={t("queueOtherCta")}
|
otherLabel={t("queueOtherCta")}
|
||||||
|
countsLine={t("queueCounts", {
|
||||||
|
here: queues.cmsOpen,
|
||||||
|
other: queues.helpOpen,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
<AdminTicketDetail
|
<AdminTicketDetail
|
||||||
ticket={{
|
ticket={{
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { asc, eq, inArray } from "drizzle-orm";
|
import { asc, eq, inArray } from "drizzle-orm";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
import { AdminHelpTicketDetail } from "@/app/admin/help-tickets/[id]/admin-help-ticket-detail";
|
import { AdminHelpTicketDetail } from "@/app/admin/help-tickets/[id]/admin-help-ticket-detail";
|
||||||
|
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||||
import { requireModPermission } from "@/lib/admin/guard";
|
import { requireModPermission } from "@/lib/admin/guard";
|
||||||
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
||||||
|
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
|
||||||
import { positiveBigInt } from "@/lib/api";
|
import { positiveBigInt } from "@/lib/api";
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
@@ -108,27 +111,41 @@ export default async function ModHelpTicketDetailPage({
|
|||||||
canAccess(permissions, PERMS.MOD_TICKETS_EDIT, session.user.rank) ||
|
canAccess(permissions, PERMS.MOD_TICKETS_EDIT, session.user.rank) ||
|
||||||
canAccess(permissions, PERMS.TICKETS_EDIT, session.user.rank);
|
canAccess(permissions, PERMS.TICKETS_EDIT, session.user.rank);
|
||||||
|
|
||||||
|
const t = await getTranslations("pages.admin.helpTickets");
|
||||||
|
const queues = await fetchTicketQueueOpenCounts();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminHelpTicketDetail
|
<div className="space-y-6">
|
||||||
ticket={{
|
<TicketQueueBanner
|
||||||
id: String(ticket.id),
|
hint={t("queueHint")}
|
||||||
title: ticket.title,
|
otherHref="/mod/tickets"
|
||||||
open: ticket.open,
|
otherLabel={t("queueOtherCta")}
|
||||||
createdAt: (ticket.createdAt ?? new Date()).toISOString(),
|
countsLine={t("queueCounts", {
|
||||||
updatedAt: (ticket.updatedAt ?? new Date()).toISOString(),
|
here: queues.helpOpen,
|
||||||
creator: creator
|
other: queues.cmsOpen,
|
||||||
? {
|
})}
|
||||||
id: creator.id,
|
/>
|
||||||
username: creator.username,
|
<AdminHelpTicketDetail
|
||||||
rank: creator.rank,
|
ticket={{
|
||||||
mail: creator.mail ?? "",
|
id: String(ticket.id),
|
||||||
}
|
title: ticket.title,
|
||||||
: null,
|
open: ticket.open,
|
||||||
}}
|
createdAt: (ticket.createdAt ?? new Date()).toISOString(),
|
||||||
messages={messages}
|
updatedAt: (ticket.updatedAt ?? new Date()).toISOString(),
|
||||||
canEdit={canEdit}
|
creator: creator
|
||||||
listHref="/mod/help-tickets"
|
? {
|
||||||
hideRequesterContact
|
id: creator.id,
|
||||||
/>
|
username: creator.username,
|
||||||
|
rank: creator.rank,
|
||||||
|
mail: creator.mail ?? "",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
}}
|
||||||
|
messages={messages}
|
||||||
|
canEdit={canEdit}
|
||||||
|
listHref="/mod/help-tickets"
|
||||||
|
hideRequesterContact
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { asc, eq, gte } from "drizzle-orm";
|
import { asc, eq, gte } from "drizzle-orm";
|
||||||
import { alias } from "drizzle-orm/mysql-core";
|
import { alias } from "drizzle-orm/mysql-core";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
import { AdminTicketDetail } from "@/app/admin/tickets/[id]/admin-ticket-detail";
|
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 { requireModPermission } from "@/lib/admin/guard";
|
||||||
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
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 { db, User, WebsiteTicket, WebsiteTicketMessage } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
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.MOD_TICKETS_EDIT, session.user.rank) ||
|
||||||
canAccess(permissions, PERMS.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 (
|
return (
|
||||||
<AdminTicketDetail
|
<div className="space-y-6">
|
||||||
ticket={{
|
<TicketQueueBanner
|
||||||
id: ticket.id,
|
hint={t("queueHint")}
|
||||||
subject: ticket.subject,
|
otherHref="/mod/help-tickets"
|
||||||
category: ticket.category,
|
otherLabel={t("queueOtherCta")}
|
||||||
priority: ticket.priority,
|
countsLine={t("queueCounts", {
|
||||||
status: ticket.status,
|
here: queues.cmsOpen,
|
||||||
createdAt: ticket.createdAt.toISOString(),
|
other: queues.helpOpen,
|
||||||
closedAt: ticket.closedAt?.toISOString() ?? null,
|
})}
|
||||||
creator: {
|
/>
|
||||||
id: ticket.creatorId,
|
<AdminTicketDetail
|
||||||
username: ticket.creatorUsername,
|
ticket={{
|
||||||
look: ticket.creatorLook,
|
id: ticket.id,
|
||||||
rank: ticket.creatorRank,
|
subject: ticket.subject,
|
||||||
mail: ticket.creatorMail ?? "",
|
category: ticket.category,
|
||||||
ipRegister: ticket.creatorIpRegister,
|
priority: ticket.priority,
|
||||||
accountCreated: ticket.creatorAccountCreated,
|
status: ticket.status,
|
||||||
},
|
createdAt: ticket.createdAt.toISOString(),
|
||||||
assignee:
|
closedAt: ticket.closedAt?.toISOString() ?? null,
|
||||||
ticket.assigneeId != null && ticket.assigneeUsername
|
creator: {
|
||||||
? { id: ticket.assigneeId, username: ticket.assigneeUsername }
|
id: ticket.creatorId,
|
||||||
: null,
|
username: ticket.creatorUsername,
|
||||||
}}
|
look: ticket.creatorLook,
|
||||||
messages={messages.map((m) => ({
|
rank: ticket.creatorRank,
|
||||||
id: m.id,
|
mail: ticket.creatorMail ?? "",
|
||||||
message: m.message,
|
ipRegister: ticket.creatorIpRegister,
|
||||||
isStaff: m.isStaff === 1,
|
accountCreated: ticket.creatorAccountCreated,
|
||||||
createdAt: m.createdAt.toISOString(),
|
},
|
||||||
user: {
|
assignee:
|
||||||
id: m.userId,
|
ticket.assigneeId != null && ticket.assigneeUsername
|
||||||
username: m.username,
|
? { id: ticket.assigneeId, username: ticket.assigneeUsername }
|
||||||
look: m.look,
|
: null,
|
||||||
rank: m.rank,
|
}}
|
||||||
},
|
messages={messages.map((m) => ({
|
||||||
}))}
|
id: m.id,
|
||||||
staffMembers={staffMembers}
|
message: m.message,
|
||||||
currentUserId={session.user.id}
|
isStaff: m.isStaff === 1,
|
||||||
canEdit={canEdit}
|
createdAt: m.createdAt.toISOString(),
|
||||||
listHref="/mod/tickets"
|
user: {
|
||||||
userLinkMode="public"
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+1
-1
@@ -24,7 +24,7 @@ import {
|
|||||||
varchar,
|
varchar,
|
||||||
} from "drizzle-orm/mysql-core";
|
} 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.
|
// for TIME). Mirror that so existing call sites keep working unchanged.
|
||||||
const timeAsDate = customType<{ data: Date; driverData: string }>({
|
const timeAsDate = customType<{ data: Date; driverData: string }>({
|
||||||
dataType() {
|
dataType() {
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ describe("positiveBigInt", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("pagination", () => {
|
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" });
|
const params = new URLSearchParams({ page: "2.5", perPage: "10.1" });
|
||||||
|
|
||||||
expect(pagination(params, 20, 100)).toEqual({
|
expect(pagination(params, 20, 100)).toEqual({
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { NextResponse } from "next/server";
|
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
|
* to strings — JSON.stringify throws on BigInt otherwise — and sets CORS to
|
||||||
* APP_URL so the game client / external integrations can read it.
|
* APP_URL so the game client / external integrations can read it.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ export function isBanActive(
|
|||||||
return banExpire === 0 || banExpire > now;
|
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()) {
|
export function activeBanWhere(now: number = unixNow()) {
|
||||||
return {
|
return {
|
||||||
OR: [{ banExpire: 0 }, { banExpire: { gt: now } }],
|
OR: [{ banExpire: 0 }, { banExpire: { gt: now } }],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import NextAuth from "next-auth";
|
|||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
|
|
||||||
// Proxy authentication must only decode the Auth.js session. Importing the
|
// 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({
|
export const { auth: proxyAuth } = NextAuth({
|
||||||
trustHost: true,
|
trustHost: true,
|
||||||
secret: env.AUTH_SECRET,
|
secret: env.AUTH_SECRET,
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export async function loadCatalogItemsData(
|
|||||||
): Promise<CatalogItemsData> {
|
): Promise<CatalogItemsData> {
|
||||||
// Load items via raw query to work around pageId Int vs VARCHAR mismatch
|
// Load items via raw query to work around pageId Int vs VARCHAR mismatch
|
||||||
const pageIdStr = String(pageId);
|
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`
|
const [rawItems] = (await db.execute(sql`
|
||||||
SELECT * FROM catalog_items
|
SELECT * FROM catalog_items
|
||||||
WHERE CAST(page_id AS CHAR) = ${pageIdStr}
|
WHERE CAST(page_id AS CHAR) = ${pageIdStr}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function toInt(value: unknown, fallback = 0): number {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Count catalog_items per page via raw SQL.
|
* 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(
|
export async function getCatalogItemCounts(
|
||||||
pageIds?: number[],
|
pageIds?: number[],
|
||||||
@@ -216,7 +216,7 @@ export async function movePage(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete catalog_items for the given page ids.
|
* 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<void> {
|
async function deleteCatalogItemsByPageIds(pageIds: number[]): Promise<void> {
|
||||||
if (pageIds.length === 0) return;
|
if (pageIds.length === 0) return;
|
||||||
|
|||||||
@@ -55,10 +55,13 @@ describe("staff smoke contract", () => {
|
|||||||
expect(readFileSync("scripts/apply-migrations.ts", "utf8")).toContain(
|
expect(readFileSync("scripts/apply-migrations.ts", "utf8")).toContain(
|
||||||
"drizzle/migrations",
|
"drizzle/migrations",
|
||||||
);
|
);
|
||||||
expect(readFileSync("package.json", "utf8")).not.toContain('"prisma"');
|
const pkg = readFileSync("package.json", "utf8");
|
||||||
expect(readFileSync("package.json", "utf8")).not.toContain(
|
expect(pkg).not.toContain('"prisma"');
|
||||||
'"@prisma/client"',
|
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(
|
expect(readFileSync("drizzle.config.ts", "utf8")).toContain(
|
||||||
"./drizzle/drafts",
|
"./drizzle/drafts",
|
||||||
);
|
);
|
||||||
@@ -144,9 +147,16 @@ describe("staff smoke contract", () => {
|
|||||||
"src/app/admin/help-tickets/page.tsx",
|
"src/app/admin/help-tickets/page.tsx",
|
||||||
"src/app/mod/tickets/desk/page.tsx",
|
"src/app/mod/tickets/desk/page.tsx",
|
||||||
"src/app/mod/help-tickets/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(existsSync(path), path).toBe(true);
|
||||||
expect(readFileSync(path, "utf8"), path).toContain("TicketQueueBanner");
|
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/page.tsx")).toBe(true);
|
||||||
expect(existsSync("src/app/admin/items/[id]/page.tsx")).toBe(true);
|
expect(existsSync("src/app/admin/items/[id]/page.tsx")).toBe(true);
|
||||||
expect(readFileSync("src/actions/items-base.ts", "utf8")).toContain(
|
expect(readFileSync("src/actions/items-base.ts", "utf8")).toContain(
|
||||||
|
|||||||
@@ -1351,6 +1351,7 @@
|
|||||||
"photos": {
|
"photos": {
|
||||||
"title": "Photo Moderation",
|
"title": "Photo Moderation",
|
||||||
"subtitle": "Recent in-game camera photos",
|
"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",
|
"delete": "Delete",
|
||||||
"approve": "Approve",
|
"approve": "Approve",
|
||||||
"noPhotos": "No photos to moderate",
|
"noPhotos": "No photos to moderate",
|
||||||
|
|||||||
@@ -1326,6 +1326,7 @@
|
|||||||
"photos": {
|
"photos": {
|
||||||
"title": "Moderazione foto",
|
"title": "Moderazione foto",
|
||||||
"subtitle": "Foto recenti dalla fotocamera di gioco",
|
"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",
|
"delete": "Elimina",
|
||||||
"approve": "Approva",
|
"approve": "Approva",
|
||||||
"noPhotos": "Nessuna foto da moderare",
|
"noPhotos": "Nessuna foto da moderare",
|
||||||
|
|||||||
Reference in new issue
Block a user