fix(admin): polish tickets, photos purge note, drizzle contracts
CI / check (push) Failing after 9s
CI / release (push) Skipped
CI / deploy (push) Skipped

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:
SimoandCursor committed 2026-08-01 15:07:54 +02:00
1 parent ba82789166
commit 3bd712e744
18 files changed
+148 -78

No files matched your search

+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 "";
+1 -1
View File
@@ -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 "";
+6
View File
@@ -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 (
<div className="space-y-6">
@@ -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,
})}
/>
<AdminHelpTicketDetail
ticket={{
+3
View File
@@ -60,6 +60,9 @@ export default async function AdminPhotos({
return (
<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">
<StatusCard label={t("photosShown")} value={photos.length} icon="📸" />
<StatusCard
+6
View File
@@ -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 { db, User, WebsiteTicket } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { AdminTicketDetail } from "./admin-ticket-detail";
@@ -59,6 +60,7 @@ export default async function AdminTicketDetailPage({
.orderBy(asc(User.username));
const canEdit = canAccess(permissions, PERMS.TICKETS_EDIT, session.user.rank);
const queues = await fetchTicketQueueOpenCounts();
return (
<div className="space-y-6">
@@ -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,
})}
/>
<AdminTicketDetail
ticket={{
+38 -21
View File
@@ -1,8 +1,11 @@
import { asc, eq, inArray } from "drizzle-orm";
import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server";
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 { getMinStaffRank } from "@/lib/admin/min-staff-rank";
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
import { positiveBigInt } from "@/lib/api";
import {
db,
@@ -108,27 +111,41 @@ export default async function ModHelpTicketDetailPage({
canAccess(permissions, PERMS.MOD_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 (
<AdminHelpTicketDetail
ticket={{
id: String(ticket.id),
title: ticket.title,
open: ticket.open,
createdAt: (ticket.createdAt ?? new Date()).toISOString(),
updatedAt: (ticket.updatedAt ?? new Date()).toISOString(),
creator: creator
? {
id: creator.id,
username: creator.username,
rank: creator.rank,
mail: creator.mail ?? "",
}
: null,
}}
messages={messages}
canEdit={canEdit}
listHref="/mod/help-tickets"
hideRequesterContact
/>
<div className="space-y-6">
<TicketQueueBanner
hint={t("queueHint")}
otherHref="/mod/tickets"
otherLabel={t("queueOtherCta")}
countsLine={t("queueCounts", {
here: queues.helpOpen,
other: queues.cmsOpen,
})}
/>
<AdminHelpTicketDetail
ticket={{
id: String(ticket.id),
title: ticket.title,
open: ticket.open,
createdAt: (ticket.createdAt ?? new Date()).toISOString(),
updatedAt: (ticket.updatedAt ?? new Date()).toISOString(),
creator: creator
? {
id: creator.id,
username: creator.username,
rank: creator.rank,
mail: creator.mail ?? "",
}
: null,
}}
messages={messages}
canEdit={canEdit}
listHref="/mod/help-tickets"
hideRequesterContact
/>
</div>
);
}
+58 -41
View File
@@ -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 (
<AdminTicketDetail
ticket={{
id: ticket.id,
subject: ticket.subject,
category: ticket.category,
priority: ticket.priority,
status: ticket.status,
createdAt: ticket.createdAt.toISOString(),
closedAt: ticket.closedAt?.toISOString() ?? null,
creator: {
id: ticket.creatorId,
username: ticket.creatorUsername,
look: ticket.creatorLook,
rank: ticket.creatorRank,
mail: ticket.creatorMail ?? "",
ipRegister: ticket.creatorIpRegister,
accountCreated: ticket.creatorAccountCreated,
},
assignee:
ticket.assigneeId != null && ticket.assigneeUsername
? { id: ticket.assigneeId, username: ticket.assigneeUsername }
: null,
}}
messages={messages.map((m) => ({
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"
/>
<div className="space-y-6">
<TicketQueueBanner
hint={t("queueHint")}
otherHref="/mod/help-tickets"
otherLabel={t("queueOtherCta")}
countsLine={t("queueCounts", {
here: queues.cmsOpen,
other: queues.helpOpen,
})}
/>
<AdminTicketDetail
ticket={{
id: ticket.id,
subject: ticket.subject,
category: ticket.category,
priority: ticket.priority,
status: ticket.status,
createdAt: ticket.createdAt.toISOString(),
closedAt: ticket.closedAt?.toISOString() ?? null,
creator: {
id: ticket.creatorId,
username: ticket.creatorUsername,
look: ticket.creatorLook,
rank: ticket.creatorRank,
mail: ticket.creatorMail ?? "",
ipRegister: ticket.creatorIpRegister,
accountCreated: ticket.creatorAccountCreated,
},
assignee:
ticket.assigneeId != null && ticket.assigneeUsername
? { id: ticket.assigneeId, username: ticket.assigneeUsername }
: null,
}}
messages={messages.map((m) => ({
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"
/>
</div>
);
}