diff --git a/src/actions/permissions.ts b/src/actions/permissions.ts index fe98c15e..e0c29652 100644 --- a/src/actions/permissions.ts +++ b/src/actions/permissions.ts @@ -149,3 +149,96 @@ export const setCmsPermissions = adminAction( return actionOk(); }, ); + +/** + * Re-apply the same grant repair as migration 0018: + * - ranks with admin.dashboard get all admin.* + * - ranks >= 6 get admin.*.view + dashboard + * - ranks >= 7 get edit/manage/execute tools used by the sidebar + */ +export const repairAdminNavAclGrants = adminAction( + { permission: PERMS.PERMISSIONS_MANAGE }, + async (ctx) => { + const dashboardFill = await prisma.$executeRaw` + INSERT INTO \`acl_model_permissions\` (\`model_type\`, \`model_id\`, \`permission_id\`) + SELECT 'Role', ar.id, ap.id + FROM \`acl_roles\` ar + JOIN \`acl_permissions\` ap ON ap.slug LIKE 'admin.%' + WHERE EXISTS ( + SELECT 1 + FROM \`acl_model_permissions\` amp + JOIN \`acl_permissions\` apdash ON apdash.id = amp.permission_id + WHERE amp.model_type = 'Role' + AND amp.model_id = ar.id + AND apdash.slug = 'admin.dashboard' + ) + AND NOT EXISTS ( + SELECT 1 + FROM \`acl_model_permissions\` amp2 + WHERE amp2.model_type = 'Role' + AND amp2.model_id = ar.id + AND amp2.permission_id = ap.id + ) + `; + + const midRankViews = await prisma.$executeRaw` + INSERT INTO \`acl_model_permissions\` (\`model_type\`, \`model_id\`, \`permission_id\`) + SELECT 'Role', ar.id, ap.id + FROM \`permission_ranks\` pr + JOIN \`acl_roles\` ar ON ar.slug = CONCAT('rank_', pr.id) + JOIN \`acl_permissions\` ap ON ( + ap.slug = 'admin.dashboard' + OR (ap.slug LIKE 'admin.%' AND ap.slug LIKE '%.view') + ) + WHERE pr.id >= 6 + AND NOT EXISTS ( + SELECT 1 + FROM \`acl_model_permissions\` amp + WHERE amp.model_type = 'Role' + AND amp.model_id = ar.id + AND amp.permission_id = ap.id + ) + `; + + const highRankTools = await prisma.$executeRaw` + INSERT INTO \`acl_model_permissions\` (\`model_type\`, \`model_id\`, \`permission_id\`) + SELECT 'Role', ar.id, ap.id + FROM \`permission_ranks\` pr + JOIN \`acl_roles\` ar ON ar.slug = CONCAT('rank_', pr.id) + JOIN \`acl_permissions\` ap ON ( + (ap.slug LIKE 'admin.%' AND ap.slug LIKE '%.edit') + OR ap.slug IN ( + 'admin.permissions.manage', + 'admin.rcon.execute', + 'admin.assets.import', + 'admin.export', + 'admin.analytics.export', + 'admin.users.ban', + 'admin.users.reset_password', + 'admin.room.delete' + ) + ) + WHERE pr.id >= 7 + AND NOT EXISTS ( + SELECT 1 + FROM \`acl_model_permissions\` amp + WHERE amp.model_type = 'Role' + AND amp.model_id = ar.id + AND amp.permission_id = ap.id + ) + `; + + const inserted = + Number(dashboardFill) + Number(midRankViews) + Number(highRankTools); + + await logStaffActivity({ + staffId: ctx.session.user.id, + action: "acl_nav_grants_repair", + description: `Repaired admin nav ACL grants (${inserted} rows inserted)`, + targetType: "acl", + targetId: 0, + }); + revalidateTag("permissions", { expire: 0 }); + return actionOk({ inserted }); + }, +); diff --git a/src/app/admin/commandocentrum/page.tsx b/src/app/admin/commandocentrum/page.tsx index 8b45fbf6..08df04c2 100644 --- a/src/app/admin/commandocentrum/page.tsx +++ b/src/app/admin/commandocentrum/page.tsx @@ -7,6 +7,7 @@ import { OnlineUsersWidget, StatusCard, } from "@/components/admin/dashboard"; +import { fetchOpsOnlineUsers } from "@/lib/admin/ops-online-users"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; @@ -46,16 +47,14 @@ export default async function CommandoCentrum() { const t = await getTranslations("pages.admin.commandocentrum"); const [ - onlineCount, totalUsers, errors, dbOk, emulatorOnline, maintenance, activity, - onlineUsersRaw, + onlineRoster, ] = await Promise.all([ - prisma.user.count({ where: { online: "1" } }).catch(() => 0), prisma.user.count().catch(() => -1), prisma.emulatorErrors .findMany({ orderBy: { timestamp: "desc" }, take: 50 }) @@ -71,29 +70,11 @@ export default async function CommandoCentrum() { .catch( () => [] as Awaited>, ), - prisma.user - .findMany({ - where: { online: "1" }, - select: { - id: true, - username: true, - look: true, - lastOnline: true, - }, - orderBy: { lastOnline: "desc" }, - take: 40, - }) - .catch(() => []), + fetchOpsOnlineUsers(40), ]); - const onlineUsers = onlineUsersRaw.map((u) => ({ - id: u.id, - username: u.username, - look: u.look, - roomId: null as number | null, - roomName: null as string | null, - lastActivity: u.lastOnline > 0 ? new Date(u.lastOnline * 1000) : new Date(), - })); + const onlineCount = onlineRoster.count; + const onlineUsers = onlineRoster.users; const staffIds = Array.from( new Set(activity.map((a) => Number(a.userId))), @@ -155,7 +136,7 @@ export default async function CommandoCentrum() { {/* ── Online users widget ───────────────────────────────── */} -
+

{t("onlineUsers")}

diff --git a/src/app/admin/devops/page.tsx b/src/app/admin/devops/page.tsx index 79d9a086..f166aba4 100644 --- a/src/app/admin/devops/page.tsx +++ b/src/app/admin/devops/page.tsx @@ -1,4 +1,5 @@ import { AlertTriangle, Database, HeartPulse, Server } from "lucide-react"; +import Link from "next/link"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { Badge } from "@/components/ui/badge"; @@ -100,8 +101,13 @@ export default async function DevOpsPage() {
{data.onlineUsers}
-

- Current active connections +

+ + Live roster in CommandoCentrum +

diff --git a/src/app/admin/help-tickets/page.tsx b/src/app/admin/help-tickets/page.tsx index 01e64a89..0adec5d4 100644 --- a/src/app/admin/help-tickets/page.tsx +++ b/src/app/admin/help-tickets/page.tsx @@ -1,6 +1,7 @@ import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { StatusCard } from "@/components/admin/dashboard"; +import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner"; import type { Prisma } from "@/generated/prisma/client"; import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { formatDate } from "@/lib/format-date"; @@ -160,20 +161,11 @@ export default async function AdminHelpTicketsPage({ return (
-
-

{t("queueHint")}

-

- - {t("queueOtherCta")} - -

-
+
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 64675d83..90d04776 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -83,13 +83,22 @@ export default async function AdminDashboard() {
- } - /> + + } + /> + {/* Header */} -
+

{ranks.length} ranks · {totalUsers.toLocaleString()} users total

- +
+ + +
{/* Rank cards */} diff --git a/src/app/admin/tickets/page.tsx b/src/app/admin/tickets/page.tsx index 97139634..258ca191 100644 --- a/src/app/admin/tickets/page.tsx +++ b/src/app/admin/tickets/page.tsx @@ -1,6 +1,7 @@ import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { StatusCard } from "@/components/admin/dashboard"; +import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner"; import type { Prisma } from "@/generated/prisma/client"; import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { formatDate } from "@/lib/format-date"; @@ -116,20 +117,11 @@ export default async function AdminTicketsPage({ return (
-
-

{t("queueHint")}

-

- - {t("queueOtherCta")} - -

-
+
+
diff --git a/src/app/mod/layout.tsx b/src/app/mod/layout.tsx index b98c8ccf..08a109c2 100644 --- a/src/app/mod/layout.tsx +++ b/src/app/mod/layout.tsx @@ -6,6 +6,7 @@ import { Radio, Shield, Ticket, + UserRound, Users, } from "lucide-react"; import Link from "next/link"; @@ -57,6 +58,9 @@ export default async function ModLayout({ children }: { children: ReactNode }) { const showTeam = canAccess(permissions, PERMS.MOD_TEAM_VIEW, staff.rank) || canAccess(permissions, PERMS.MODERATION_VIEW, staff.rank); + const showUsers = + canAccess(permissions, PERMS.MOD_USERS_VIEW, staff.rank) || + canAccess(permissions, PERMS.USERS_VIEW, staff.rank); const showAdmin = canAccess(permissions, PERMS.ADMIN_DASHBOARD, staff.rank); const nav = [ @@ -81,6 +85,12 @@ export default async function ModLayout({ children }: { children: ReactNode }) { show: showActions, }, { href: "/mod/bans", label: t("nav.bans"), icon: Ban, show: showBans }, + { + href: "/mod/users", + label: t("nav.users"), + icon: UserRound, + show: showUsers, + }, { href: "/mod/team", label: t("nav.team"), icon: Users, show: showTeam }, ].filter((item) => item.show); diff --git a/src/app/mod/page.tsx b/src/app/mod/page.tsx index 4282f7d8..43968be7 100644 --- a/src/app/mod/page.tsx +++ b/src/app/mod/page.tsx @@ -37,6 +37,9 @@ export default async function ModDashboardPage() { const showTeam = canAccess(permissions, PERMS.MOD_TEAM_VIEW, rank) || canAccess(permissions, PERMS.MODERATION_VIEW, rank); + const showUsers = + canAccess(permissions, PERMS.MOD_USERS_VIEW, rank) || + canAccess(permissions, PERMS.USERS_VIEW, rank); const [openCfh, todayBans, modsOnline, openTickets, openHelpTickets] = await Promise.all([ @@ -178,6 +181,13 @@ export default async function ModDashboardPage() { ) : null} + {showUsers ? ( +
  • + + {t("nav.users")} + +
  • + ) : null} {showTeam ? (
  • diff --git a/src/app/mod/tickets/page.tsx b/src/app/mod/tickets/page.tsx index 961ac850..6fbd8b3d 100644 --- a/src/app/mod/tickets/page.tsx +++ b/src/app/mod/tickets/page.tsx @@ -4,6 +4,7 @@ import { TicketsTable, } from "@/app/admin/tickets/tickets-table"; import { StatusCard } from "@/components/admin/dashboard"; +import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner"; import type { Prisma } from "@/generated/prisma/client"; import { requireModPermission } from "@/lib/admin/guard"; import { calcPagination, parseListParams } from "@/lib/admin-helpers"; @@ -116,6 +117,11 @@ export default async function ModTicketsPage({ return (
    +
    ; +}) { + await requireModPermission([PERMS.MOD_USERS_VIEW, PERMS.USERS_VIEW]); + const t = await getTranslations("pages.mod"); + const { id: rawId } = await params; + const id = Number(rawId); + if (!Number.isFinite(id) || id <= 0) notFound(); + + const user = await prisma.user.findUnique({ + where: { id }, + select: { + id: true, + username: true, + rank: true, + online: true, + motto: true, + look: true, + accountCreated: true, + lastLogin: true, + lastOnline: true, + }, + }); + if (!user) notFound(); + + return ( +
    +
    +
    + +
    +

    {user.username}

    +

    + #{user.id} · {t("usersRank")} {user.rank} +

    +
    + {user.online === "1" ? ( + Online + ) : ( + Offline + )} +
    +
    +
    +
    + + ← {t("nav.users")} + + + {t("usersPublicProfile")} + +
    +
    + +
    + {t("usersPiiNote")} +
    + +
    +
    +

    + Motto +

    +

    {user.motto || "—"}

    +
    +
    +

    + {t("usersJoined")} +

    +

    + {user.accountCreated > 0 + ? formatDate(new Date(user.accountCreated * 1000), "date") + : "—"} +

    +
    +
    +

    + {t("usersLastLogin")} +

    +

    + {user.lastLogin + ? formatDate(new Date(user.lastLogin * 1000), "datetime") + : "—"} +

    +
    +
    +

    + {t("usersLastOnline")} +

    +

    + {user.lastOnline > 0 + ? formatDate(new Date(user.lastOnline * 1000), "datetime") + : "—"} +

    +
    +
    +
    + ); +} diff --git a/src/app/mod/users/page.tsx b/src/app/mod/users/page.tsx new file mode 100644 index 00000000..e358fb35 --- /dev/null +++ b/src/app/mod/users/page.tsx @@ -0,0 +1,170 @@ +import Link from "next/link"; +import { getTranslations } from "next-intl/server"; +import { DataTable } from "@/components/admin/data-table"; +import { Badge } from "@/components/ui/badge"; +import { requireModPermission } from "@/lib/admin/guard"; +import { calcPagination, parseListParams } from "@/lib/admin-helpers"; +import { getAvatarUrl } from "@/lib/imager"; +import { PERMS } from "@/lib/permissions"; +import { prisma } from "@/lib/prisma"; +import type { DataTableColumn } from "@/types/common"; + +export const dynamic = "force-dynamic"; + +type ModUserRow = { + id: number; + username: string; + rank: number; + online: string; + motto: string; + look: string; + lastLogin: number | null; +}; + +const columns: DataTableColumn[] = [ + { key: "id", label: "ID", sortable: true }, + { + key: "username", + label: "User", + sortable: true, + render: (_, row) => ( +
    + + {row.username} +
    + ), + }, + { + key: "rank", + label: "Rank", + sortable: true, + render: (v) => #{String(v)}, + }, + { + key: "online", + label: "Online", + render: (v) => + v === "1" ? ( + Online + ) : ( + Offline + ), + }, + { + key: "motto", + label: "Motto", + render: (v) => ( + + {String(v || "—")} + + ), + }, + { + key: "actions", + label: "", + render: (_, row) => ( +
    + + Mod view + + + Public + +
    + ), + }, +]; + +export default async function ModUsersPage({ + searchParams, +}: { + searchParams: Promise>; +}) { + await requireModPermission([PERMS.MOD_USERS_VIEW, PERMS.USERS_VIEW]); + const t = await getTranslations("pages.mod"); + const raw = await searchParams; + const parsed = parseListParams(new URLSearchParams(raw)); + + const where = parsed.search.trim() + ? { + OR: [ + { username: { contains: parsed.search.trim() } }, + { motto: { contains: parsed.search.trim() } }, + ...(Number.isFinite(Number(parsed.search)) && + Number(parsed.search) > 0 + ? [{ id: Number(parsed.search) }] + : []), + ], + } + : {}; + + const orderBy = parsed.sort + ? { [parsed.sort]: parsed.order } + : { id: "desc" as const }; + + const [total, users] = await Promise.all([ + prisma.user.count({ where }).catch(() => 0), + prisma.user + .findMany({ + where, + orderBy, + skip: (parsed.page - 1) * parsed.perPage, + take: parsed.perPage, + select: { + id: true, + username: true, + rank: true, + online: true, + motto: true, + look: true, + lastLogin: true, + }, + }) + .catch(() => []), + ]); + + const pagination = calcPagination(total, parsed.page, parsed.perPage); + const rows: ModUserRow[] = users; + + return ( +
    +
    +

    {t("usersTitle")}

    +

    + {t("usersSubtitle")} +

    +
    + {total === 0 && !parsed.search ? ( +
    {t("usersEmpty")}
    + ) : ( + + )} +
    + ); +} diff --git a/src/components/admin/ticket-queue-banner.tsx b/src/components/admin/ticket-queue-banner.tsx new file mode 100644 index 00000000..0cf88947 --- /dev/null +++ b/src/components/admin/ticket-queue-banner.tsx @@ -0,0 +1,29 @@ +import Link from "next/link"; + +/** Shared clarity banner for dual ticket products (CMS desk vs help-center). */ +export function TicketQueueBanner({ + hint, + otherHref, + otherLabel, +}: { + hint: string; + otherHref: string; + otherLabel: string; +}) { + return ( +
    +

    {hint}

    +

    + + {otherLabel} + +

    +
    + ); +} diff --git a/src/lib/admin-operations-contract.test.ts b/src/lib/admin-operations-contract.test.ts index ac282399..25c3ee16 100644 --- a/src/lib/admin-operations-contract.test.ts +++ b/src/lib/admin-operations-contract.test.ts @@ -50,6 +50,7 @@ const MOD_ROUTES: Array<[string, string]> = [ ["bans", "PERMS.MOD_BANS_VIEW"], ["tickets", "PERMS.MOD_TICKETS_VIEW"], ["help-tickets", "PERMS.MOD_TICKETS_VIEW"], + ["users", "PERMS.MOD_USERS_VIEW"], ["team", "PERMS.MOD_TEAM_VIEW"], ]; @@ -73,6 +74,7 @@ const ACTION_GATES: Array<[string, string]> = [ ["tickets.ts", "PERMS.TICKETS_EDIT"], ["admin-help-tickets.ts", "PERMS.TICKETS_EDIT"], ["permissions.ts", "PERMS.PERMISSIONS_MANAGE"], + ["permissions.ts", "repairAdminNavAclGrants"], ["admin-guilds.ts", "PERMS.USERS_EDIT"], ["moderation.ts", "PERMS.MODERATION_EDIT"], ]; diff --git a/src/lib/admin/guard.ts b/src/lib/admin/guard.ts index 54c25544..51fc1a54 100644 --- a/src/lib/admin/guard.ts +++ b/src/lib/admin/guard.ts @@ -70,6 +70,7 @@ export async function requireMod(): Promise { canAccess(permissions, PERMS.MOD_BANS_VIEW, session.user.rank) || canAccess(permissions, PERMS.MOD_TICKETS_VIEW, session.user.rank) || canAccess(permissions, PERMS.MOD_TEAM_VIEW, session.user.rank) || + canAccess(permissions, PERMS.MOD_USERS_VIEW, session.user.rank) || canAccess(permissions, PERMS.TICKETS_VIEW, session.user.rank); if (!ok) redirectSafe("/", "/"); return { diff --git a/src/lib/admin/ops-online-users.ts b/src/lib/admin/ops-online-users.ts new file mode 100644 index 00000000..00dd793e --- /dev/null +++ b/src/lib/admin/ops-online-users.ts @@ -0,0 +1,35 @@ +import type { OnlineUser } from "@/components/admin/dashboard"; +import { prisma } from "@/lib/prisma"; + +/** Shared online roster used by CommandoCentrum (and linked from other ops hubs). */ +export async function fetchOpsOnlineUsers( + limit = 40, +): Promise<{ count: number; users: OnlineUser[] }> { + const [count, onlineUsersRaw] = await Promise.all([ + prisma.user.count({ where: { online: "1" } }).catch(() => 0), + prisma.user + .findMany({ + where: { online: "1" }, + select: { + id: true, + username: true, + look: true, + lastOnline: true, + }, + orderBy: { lastOnline: "desc" }, + take: limit, + }) + .catch(() => []), + ]); + + const users: OnlineUser[] = onlineUsersRaw.map((u) => ({ + id: u.id, + username: u.username, + look: u.look, + roomId: null, + roomName: null, + lastActivity: u.lastOnline > 0 ? new Date(u.lastOnline * 1000) : new Date(), + })); + + return { count, users }; +} diff --git a/src/messages/bg.json b/src/messages/bg.json index 9db6cffe..0b69d2ee 100644 --- a/src/messages/bg.json +++ b/src/messages/bg.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/cs.json b/src/messages/cs.json index 56d97732..e89c865b 100644 --- a/src/messages/cs.json +++ b/src/messages/cs.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/da.json b/src/messages/da.json index db04378d..f5da6cca 100644 --- a/src/messages/da.json +++ b/src/messages/da.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/de.json b/src/messages/de.json index 80129071..2bb4a88d 100644 --- a/src/messages/de.json +++ b/src/messages/de.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/el.json b/src/messages/el.json index be6d909f..a85c9cb5 100644 --- a/src/messages/el.json +++ b/src/messages/el.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/en.json b/src/messages/en.json index d9a5f79a..b40e6b93 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -3010,8 +3010,19 @@ "backToSite": "Back to site", "tickets": "CMS tickets", "helpTickets": "Help-center tickets", + "users": "Users", "team": "Team" }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online", "stats": { "openCfh": "Open CFH", "bansToday": "Bans today", diff --git a/src/messages/es.json b/src/messages/es.json index a46b043e..031946a2 100644 --- a/src/messages/es.json +++ b/src/messages/es.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/fr.json b/src/messages/fr.json index 2d96cb73..bd256aa8 100644 --- a/src/messages/fr.json +++ b/src/messages/fr.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/hr.json b/src/messages/hr.json index 5ca2b1cf..0d40e45f 100644 --- a/src/messages/hr.json +++ b/src/messages/hr.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/hu.json b/src/messages/hu.json index 08901750..82441198 100644 --- a/src/messages/hu.json +++ b/src/messages/hu.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/it.json b/src/messages/it.json index f4122a69..e3aa7593 100644 --- a/src/messages/it.json +++ b/src/messages/it.json @@ -3017,8 +3017,19 @@ "backToSite": "Torna al sito", "tickets": "Ticket CMS", "helpTickets": "Ticket help center", + "users": "Utenti", "team": "Team" }, + "usersTitle": "Giocatori", + "usersSubtitle": "Lookup senza email o IP — usa l’admin completo per i campi sensibili.", + "usersEmpty": "Nessun giocatore trovato", + "usersSearchPlaceholder": "Cerca username, motto o ID…", + "usersRank": "rank", + "usersPublicProfile": "Profilo pubblico", + "usersPiiNote": "Email, IP e valuta sono nascosti nel pannello mod. Apri l’admin completo se hai accesso.", + "usersJoined": "Iscrizione", + "usersLastLogin": "Ultimo login", + "usersLastOnline": "Ultima presenza", "stats": { "openCfh": "CFH aperti", "bansToday": "Ban oggi", diff --git a/src/messages/nl.json b/src/messages/nl.json index c61e2197..53f5a098 100644 --- a/src/messages/nl.json +++ b/src/messages/nl.json @@ -3015,8 +3015,19 @@ "backToSite": "Terug naar site", "tickets": "CMS-tickets", "helpTickets": "Helpcenter-tickets", + "users": "Gebruikers", "team": "Team" }, + "usersTitle": "Spelers", + "usersSubtitle": "Opzoeken zonder e-mail of IP — gebruik full admin voor gevoelige velden.", + "usersEmpty": "Geen spelers gevonden", + "usersSearchPlaceholder": "Zoek username, motto of ID…", + "usersRank": "rank", + "usersPublicProfile": "Openbaar profiel", + "usersPiiNote": "E-mail, IP en valuta zijn verborgen in het mod-panel. Open full admin als je toegang hebt.", + "usersJoined": "Lid sinds", + "usersLastLogin": "Laatste login", + "usersLastOnline": "Laatst online", "stats": { "openCfh": "Open CFH", "bansToday": "Bans vandaag", diff --git a/src/messages/no.json b/src/messages/no.json index 37ec9114..d751ac2e 100644 --- a/src/messages/no.json +++ b/src/messages/no.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/pl.json b/src/messages/pl.json index ec5a5400..3c81a9b8 100644 --- a/src/messages/pl.json +++ b/src/messages/pl.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/pt.json b/src/messages/pt.json index be2977ff..b5b1cb81 100644 --- a/src/messages/pt.json +++ b/src/messages/pt.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/ro.json b/src/messages/ro.json index 7da30edd..9892bfa6 100644 --- a/src/messages/ro.json +++ b/src/messages/ro.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/ru.json b/src/messages/ru.json index c4cdaf8f..d61416cb 100644 --- a/src/messages/ru.json +++ b/src/messages/ru.json @@ -3006,7 +3006,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3018,7 +3019,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/sk.json b/src/messages/sk.json index 67d336fc..8c86ee26 100644 --- a/src/messages/sk.json +++ b/src/messages/sk.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/sr.json b/src/messages/sr.json index 486d06d2..f080667d 100644 --- a/src/messages/sr.json +++ b/src/messages/sr.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/sv.json b/src/messages/sv.json index 7fdc3251..c36f6768 100644 --- a/src/messages/sv.json +++ b/src/messages/sv.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/tr.json b/src/messages/tr.json index d87ff82f..83ff59f1 100644 --- a/src/messages/tr.json +++ b/src/messages/tr.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": { diff --git a/src/messages/uk.json b/src/messages/uk.json index 429140ce..d1e67f1b 100644 --- a/src/messages/uk.json +++ b/src/messages/uk.json @@ -3008,7 +3008,8 @@ "backToSite": "Back to site", "tickets": "Tickets", "team": "Team", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "users": "Users" }, "stats": { "openCfh": "Open CFH", @@ -3020,7 +3021,17 @@ "team": "Mod team", "teamHint": "Online staff & workload", "openHelpTickets": "Open help tickets" - } + }, + "usersTitle": "Players", + "usersSubtitle": "Lookup without email or IP — use full admin for sensitive fields.", + "usersEmpty": "No players found", + "usersSearchPlaceholder": "Search username, motto or ID…", + "usersRank": "rank", + "usersPublicProfile": "Public profile", + "usersPiiNote": "Email, IP and currency are hidden in the mod panel. Open full admin if you have access.", + "usersJoined": "Joined", + "usersLastLogin": "Last login", + "usersLastOnline": "Last online" } }, "emails": {