feat(admin): ACL repair, mod users, ticket clarity, ops online hub
Add Repair nav grants on permissions, /mod/users without email/IP, shared ticket queue banners, and shared online roster on CommandoCentrum. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
686279eb25
commit
6eab5e5343
39 files changed
+841
-106
No files matched your search
@@ -149,3 +149,96 @@ export const setCmsPermissions = adminAction(
|
|||||||
return actionOk();
|
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 });
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
OnlineUsersWidget,
|
OnlineUsersWidget,
|
||||||
StatusCard,
|
StatusCard,
|
||||||
} from "@/components/admin/dashboard";
|
} from "@/components/admin/dashboard";
|
||||||
|
import { fetchOpsOnlineUsers } from "@/lib/admin/ops-online-users";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
@@ -46,16 +47,14 @@ export default async function CommandoCentrum() {
|
|||||||
const t = await getTranslations("pages.admin.commandocentrum");
|
const t = await getTranslations("pages.admin.commandocentrum");
|
||||||
|
|
||||||
const [
|
const [
|
||||||
onlineCount,
|
|
||||||
totalUsers,
|
totalUsers,
|
||||||
errors,
|
errors,
|
||||||
dbOk,
|
dbOk,
|
||||||
emulatorOnline,
|
emulatorOnline,
|
||||||
maintenance,
|
maintenance,
|
||||||
activity,
|
activity,
|
||||||
onlineUsersRaw,
|
onlineRoster,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
prisma.user.count({ where: { online: "1" } }).catch(() => 0),
|
|
||||||
prisma.user.count().catch(() => -1),
|
prisma.user.count().catch(() => -1),
|
||||||
prisma.emulatorErrors
|
prisma.emulatorErrors
|
||||||
.findMany({ orderBy: { timestamp: "desc" }, take: 50 })
|
.findMany({ orderBy: { timestamp: "desc" }, take: 50 })
|
||||||
@@ -71,29 +70,11 @@ export default async function CommandoCentrum() {
|
|||||||
.catch(
|
.catch(
|
||||||
() => [] as Awaited<ReturnType<typeof prisma.staffActivities.findMany>>,
|
() => [] as Awaited<ReturnType<typeof prisma.staffActivities.findMany>>,
|
||||||
),
|
),
|
||||||
prisma.user
|
fetchOpsOnlineUsers(40),
|
||||||
.findMany({
|
|
||||||
where: { online: "1" },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
look: true,
|
|
||||||
lastOnline: true,
|
|
||||||
},
|
|
||||||
orderBy: { lastOnline: "desc" },
|
|
||||||
take: 40,
|
|
||||||
})
|
|
||||||
.catch(() => []),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onlineUsers = onlineUsersRaw.map((u) => ({
|
const onlineCount = onlineRoster.count;
|
||||||
id: u.id,
|
const onlineUsers = onlineRoster.users;
|
||||||
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 staffIds = Array.from(
|
const staffIds = Array.from(
|
||||||
new Set(activity.map((a) => Number(a.userId))),
|
new Set(activity.map((a) => Number(a.userId))),
|
||||||
@@ -155,7 +136,7 @@ export default async function CommandoCentrum() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Online users widget ───────────────────────────────── */}
|
{/* ── Online users widget ───────────────────────────────── */}
|
||||||
<section className="admin-card mb-6">
|
<section id="online" className="admin-card mb-6 scroll-mt-4">
|
||||||
<h2 className="admin-section-title">{t("onlineUsers")}</h2>
|
<h2 className="admin-section-title">{t("onlineUsers")}</h2>
|
||||||
<OnlineUsersWidget users={onlineUsers} isLoading={false} />
|
<OnlineUsersWidget users={onlineUsers} isLoading={false} />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AlertTriangle, Database, HeartPulse, Server } from "lucide-react";
|
import { AlertTriangle, Database, HeartPulse, Server } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -100,8 +101,13 @@ export default async function DevOpsPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{data.onlineUsers}</div>
|
<div className="text-2xl font-bold">{data.onlineUsers}</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Current active connections
|
<Link
|
||||||
|
href="/admin/commandocentrum#online"
|
||||||
|
className="text-[var(--admin-accent)] underline-offset-2 hover:underline"
|
||||||
|
>
|
||||||
|
Live roster in CommandoCentrum
|
||||||
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
|
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||||
import type { Prisma } from "@/generated/prisma/client";
|
import type { Prisma } from "@/generated/prisma/client";
|
||||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
@@ -160,20 +161,11 @@ export default async function AdminHelpTicketsPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div
|
<TicketQueueBanner
|
||||||
className="rounded-lg border border-[var(--admin-info-border)] bg-[var(--admin-info-subtle)] px-4 py-3 text-sm"
|
hint={t("queueHint")}
|
||||||
role="status"
|
otherHref="/admin/tickets"
|
||||||
>
|
otherLabel={t("queueOtherCta")}
|
||||||
<p className="m-0">{t("queueHint")}</p>
|
/>
|
||||||
<p className="m-0 mt-1">
|
|
||||||
<a
|
|
||||||
href="/admin/tickets"
|
|
||||||
className="font-medium text-[var(--admin-accent)] underline-offset-2 hover:underline"
|
|
||||||
>
|
|
||||||
{t("queueOtherCta")}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
|
||||||
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
||||||
<StatusCard label={t("statusClosed")} value={closedCount} icon="✅" />
|
<StatusCard label={t("statusClosed")} value={closedCount} icon="✅" />
|
||||||
|
|||||||
+16
-7
@@ -83,13 +83,22 @@ export default async function AdminDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||||
<StatusCard
|
<Link
|
||||||
label={t("playersOnline")}
|
href="/admin/commandocentrum#online"
|
||||||
value={online}
|
className="no-underline text-inherit"
|
||||||
hint={dbOk ? t("totalAccounts", { count: users }) : t("databaseDown")}
|
>
|
||||||
state={dbOk ? "neutral" : "danger"}
|
<StatusCard
|
||||||
icon={<Users size={18} />}
|
label={t("playersOnline")}
|
||||||
/>
|
value={online}
|
||||||
|
hint={
|
||||||
|
dbOk
|
||||||
|
? `${t("totalAccounts", { count: users })} · live roster →`
|
||||||
|
: t("databaseDown")
|
||||||
|
}
|
||||||
|
state={dbOk ? "neutral" : "danger"}
|
||||||
|
icon={<Users size={18} />}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
<StatusCard
|
<StatusCard
|
||||||
label={t("articles")}
|
label={t("articles")}
|
||||||
value={dbOk ? articles : "—"}
|
value={dbOk ? articles : "—"}
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import {
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { createRank, deleteRank } from "@/actions/permissions";
|
import {
|
||||||
|
createRank,
|
||||||
|
deleteRank,
|
||||||
|
repairAdminNavAclGrants,
|
||||||
|
} from "@/actions/permissions";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -123,15 +127,30 @@ export function PermissionsList({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{ranks.length} ranks · {totalUsers.toLocaleString()} users
|
{ranks.length} ranks · {totalUsers.toLocaleString()} users
|
||||||
total
|
total
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={() => setShowCreate(true)}>
|
<div className="flex flex-wrap gap-2">
|
||||||
<Plus className="h-4 w-4 mr-1.5" />
|
<Button
|
||||||
New Rank
|
variant="outline"
|
||||||
</Button>
|
disabled={isPending}
|
||||||
|
onClick={() =>
|
||||||
|
run(() => repairAdminNavAclGrants(), {
|
||||||
|
successMessage: "Admin nav ACL grants repaired",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
title="Fill missing admin.* grants for ranks that already have dashboard access (same as migration 0018)"
|
||||||
|
>
|
||||||
|
<ShieldCheck className="h-4 w-4 mr-1.5" />
|
||||||
|
Repair nav grants
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setShowCreate(true)}>
|
||||||
|
<Plus className="h-4 w-4 mr-1.5" />
|
||||||
|
New Rank
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rank cards */}
|
{/* Rank cards */}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
|
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||||
import type { Prisma } from "@/generated/prisma/client";
|
import type { Prisma } from "@/generated/prisma/client";
|
||||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
@@ -116,20 +117,11 @@ export default async function AdminTicketsPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div
|
<TicketQueueBanner
|
||||||
className="rounded-lg border border-[var(--admin-info-border)] bg-[var(--admin-info-subtle)] px-4 py-3 text-sm"
|
hint={t("queueHint")}
|
||||||
role="status"
|
otherHref="/admin/help-tickets"
|
||||||
>
|
otherLabel={t("queueOtherCta")}
|
||||||
<p className="m-0">{t("queueHint")}</p>
|
/>
|
||||||
<p className="m-0 mt-1">
|
|
||||||
<a
|
|
||||||
href="/admin/help-tickets"
|
|
||||||
className="font-medium text-[var(--admin-accent)] underline-offset-2 hover:underline"
|
|
||||||
>
|
|
||||||
{t("queueOtherCta")}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
|
||||||
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
||||||
<StatusCard
|
<StatusCard
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
HelpTicketsTable,
|
HelpTicketsTable,
|
||||||
} from "@/app/admin/help-tickets/help-tickets-table";
|
} from "@/app/admin/help-tickets/help-tickets-table";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
|
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||||
import type { Prisma } from "@/generated/prisma/client";
|
import type { Prisma } from "@/generated/prisma/client";
|
||||||
import { requireModPermission } from "@/lib/admin/guard";
|
import { requireModPermission } from "@/lib/admin/guard";
|
||||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||||
@@ -160,6 +161,11 @@ export default async function ModHelpTicketsPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<TicketQueueBanner
|
||||||
|
hint={t("queueHint")}
|
||||||
|
otherHref="/mod/tickets"
|
||||||
|
otherLabel={t("queueOtherCta")}
|
||||||
|
/>
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
|
||||||
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
||||||
<StatusCard label={t("statusClosed")} value={closedCount} icon="✅" />
|
<StatusCard label={t("statusClosed")} value={closedCount} icon="✅" />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Radio,
|
Radio,
|
||||||
Shield,
|
Shield,
|
||||||
Ticket,
|
Ticket,
|
||||||
|
UserRound,
|
||||||
Users,
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -57,6 +58,9 @@ export default async function ModLayout({ children }: { children: ReactNode }) {
|
|||||||
const showTeam =
|
const showTeam =
|
||||||
canAccess(permissions, PERMS.MOD_TEAM_VIEW, staff.rank) ||
|
canAccess(permissions, PERMS.MOD_TEAM_VIEW, staff.rank) ||
|
||||||
canAccess(permissions, PERMS.MODERATION_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 showAdmin = canAccess(permissions, PERMS.ADMIN_DASHBOARD, staff.rank);
|
||||||
|
|
||||||
const nav = [
|
const nav = [
|
||||||
@@ -81,6 +85,12 @@ export default async function ModLayout({ children }: { children: ReactNode }) {
|
|||||||
show: showActions,
|
show: showActions,
|
||||||
},
|
},
|
||||||
{ href: "/mod/bans", label: t("nav.bans"), icon: Ban, show: showBans },
|
{ 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 },
|
{ href: "/mod/team", label: t("nav.team"), icon: Users, show: showTeam },
|
||||||
].filter((item) => item.show);
|
].filter((item) => item.show);
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export default async function ModDashboardPage() {
|
|||||||
const showTeam =
|
const showTeam =
|
||||||
canAccess(permissions, PERMS.MOD_TEAM_VIEW, rank) ||
|
canAccess(permissions, PERMS.MOD_TEAM_VIEW, rank) ||
|
||||||
canAccess(permissions, PERMS.MODERATION_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] =
|
const [openCfh, todayBans, modsOnline, openTickets, openHelpTickets] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -178,6 +181,13 @@ export default async function ModDashboardPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
) : null}
|
) : null}
|
||||||
|
{showUsers ? (
|
||||||
|
<li>
|
||||||
|
<Link href="/mod/users" className="text-primary hover:underline">
|
||||||
|
{t("nav.users")}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
{showTeam ? (
|
{showTeam ? (
|
||||||
<li>
|
<li>
|
||||||
<Link href="/mod/team" className="text-primary hover:underline">
|
<Link href="/mod/team" className="text-primary hover:underline">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
TicketsTable,
|
TicketsTable,
|
||||||
} from "@/app/admin/tickets/tickets-table";
|
} from "@/app/admin/tickets/tickets-table";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
|
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||||
import type { Prisma } from "@/generated/prisma/client";
|
import type { Prisma } from "@/generated/prisma/client";
|
||||||
import { requireModPermission } from "@/lib/admin/guard";
|
import { requireModPermission } from "@/lib/admin/guard";
|
||||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||||
@@ -116,6 +117,11 @@ export default async function ModTicketsPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<TicketQueueBanner
|
||||||
|
hint={t("queueHint")}
|
||||||
|
otherHref="/mod/help-tickets"
|
||||||
|
otherLabel={t("queueOtherCta")}
|
||||||
|
/>
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
|
||||||
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
||||||
<StatusCard
|
<StatusCard
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { requireModPermission } from "@/lib/admin/guard";
|
||||||
|
import { formatDate } from "@/lib/format-date";
|
||||||
|
import { getAvatarUrl } from "@/lib/imager";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export default async function ModUserDetailPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}) {
|
||||||
|
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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<img
|
||||||
|
src={getAvatarUrl(user.look, {
|
||||||
|
size: "l",
|
||||||
|
direction: 2,
|
||||||
|
headDirection: 2,
|
||||||
|
})}
|
||||||
|
alt=""
|
||||||
|
width={64}
|
||||||
|
height={110}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h2 className="admin-section-title m-0">{user.username}</h2>
|
||||||
|
<p className="mt-1 text-sm text-[var(--admin-text-muted)]">
|
||||||
|
#{user.id} · {t("usersRank")} {user.rank}
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 flex gap-2">
|
||||||
|
{user.online === "1" ? (
|
||||||
|
<Badge className="bg-[var(--admin-success)]">Online</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline">Offline</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Link
|
||||||
|
href="/mod/users"
|
||||||
|
className="text-sm text-muted-foreground underline-offset-2 hover:underline"
|
||||||
|
>
|
||||||
|
← {t("nav.users")}
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={`/u/${user.username}`}
|
||||||
|
className="text-sm text-[var(--admin-accent)] underline-offset-2 hover:underline"
|
||||||
|
>
|
||||||
|
{t("usersPublicProfile")}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="rounded-lg border border-[var(--admin-info-border)] bg-[var(--admin-info-subtle)] px-4 py-3 text-sm"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
{t("usersPiiNote")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="admin-card grid gap-4 p-5 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<p className="m-0 text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
Motto
|
||||||
|
</p>
|
||||||
|
<p className="m-0 mt-1">{user.motto || "—"}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="m-0 text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
{t("usersJoined")}
|
||||||
|
</p>
|
||||||
|
<p className="m-0 mt-1">
|
||||||
|
{user.accountCreated > 0
|
||||||
|
? formatDate(new Date(user.accountCreated * 1000), "date")
|
||||||
|
: "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="m-0 text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
{t("usersLastLogin")}
|
||||||
|
</p>
|
||||||
|
<p className="m-0 mt-1">
|
||||||
|
{user.lastLogin
|
||||||
|
? formatDate(new Date(user.lastLogin * 1000), "datetime")
|
||||||
|
: "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="m-0 text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
{t("usersLastOnline")}
|
||||||
|
</p>
|
||||||
|
<p className="m-0 mt-1">
|
||||||
|
{user.lastOnline > 0
|
||||||
|
? formatDate(new Date(user.lastOnline * 1000), "datetime")
|
||||||
|
: "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<ModUserRow>[] = [
|
||||||
|
{ key: "id", label: "ID", sortable: true },
|
||||||
|
{
|
||||||
|
key: "username",
|
||||||
|
label: "User",
|
||||||
|
sortable: true,
|
||||||
|
render: (_, row) => (
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<img
|
||||||
|
src={getAvatarUrl(row.look, {
|
||||||
|
size: "m",
|
||||||
|
direction: 2,
|
||||||
|
headDirection: 2,
|
||||||
|
})}
|
||||||
|
alt=""
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<span className="font-medium">{row.username}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "rank",
|
||||||
|
label: "Rank",
|
||||||
|
sortable: true,
|
||||||
|
render: (v) => <Badge variant="secondary">#{String(v)}</Badge>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "online",
|
||||||
|
label: "Online",
|
||||||
|
render: (v) =>
|
||||||
|
v === "1" ? (
|
||||||
|
<Badge className="bg-[var(--admin-success)]">Online</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline">Offline</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "motto",
|
||||||
|
label: "Motto",
|
||||||
|
render: (v) => (
|
||||||
|
<span className="text-muted-foreground truncate max-w-[220px] block">
|
||||||
|
{String(v || "—")}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "actions",
|
||||||
|
label: "",
|
||||||
|
render: (_, row) => (
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<Link
|
||||||
|
href={`/mod/users/${row.id}`}
|
||||||
|
className="text-sm text-[var(--admin-accent)] underline-offset-2 hover:underline"
|
||||||
|
>
|
||||||
|
Mod view
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={`/u/${row.username}`}
|
||||||
|
className="text-sm text-muted-foreground underline-offset-2 hover:underline"
|
||||||
|
>
|
||||||
|
Public
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default async function ModUsersPage({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Promise<Record<string, string>>;
|
||||||
|
}) {
|
||||||
|
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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="admin-section-title m-0">{t("usersTitle")}</h2>
|
||||||
|
<p className="mt-1 text-sm text-[var(--admin-text-muted)]">
|
||||||
|
{t("usersSubtitle")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{total === 0 && !parsed.search ? (
|
||||||
|
<div className="admin-empty">{t("usersEmpty")}</div>
|
||||||
|
) : (
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={{
|
||||||
|
rows,
|
||||||
|
total,
|
||||||
|
page: pagination.page,
|
||||||
|
perPage: pagination.perPage,
|
||||||
|
lastPage: pagination.lastPage,
|
||||||
|
}}
|
||||||
|
searchPlaceholder={t("usersSearchPlaceholder")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
className="rounded-lg border border-[var(--admin-info-border)] bg-[var(--admin-info-subtle)] px-4 py-3 text-sm"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<p className="m-0">{hint}</p>
|
||||||
|
<p className="m-0 mt-1">
|
||||||
|
<Link
|
||||||
|
href={otherHref}
|
||||||
|
className="font-medium text-[var(--admin-accent)] underline-offset-2 hover:underline"
|
||||||
|
>
|
||||||
|
{otherLabel}
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ const MOD_ROUTES: Array<[string, string]> = [
|
|||||||
["bans", "PERMS.MOD_BANS_VIEW"],
|
["bans", "PERMS.MOD_BANS_VIEW"],
|
||||||
["tickets", "PERMS.MOD_TICKETS_VIEW"],
|
["tickets", "PERMS.MOD_TICKETS_VIEW"],
|
||||||
["help-tickets", "PERMS.MOD_TICKETS_VIEW"],
|
["help-tickets", "PERMS.MOD_TICKETS_VIEW"],
|
||||||
|
["users", "PERMS.MOD_USERS_VIEW"],
|
||||||
["team", "PERMS.MOD_TEAM_VIEW"],
|
["team", "PERMS.MOD_TEAM_VIEW"],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -73,6 +74,7 @@ const ACTION_GATES: Array<[string, string]> = [
|
|||||||
["tickets.ts", "PERMS.TICKETS_EDIT"],
|
["tickets.ts", "PERMS.TICKETS_EDIT"],
|
||||||
["admin-help-tickets.ts", "PERMS.TICKETS_EDIT"],
|
["admin-help-tickets.ts", "PERMS.TICKETS_EDIT"],
|
||||||
["permissions.ts", "PERMS.PERMISSIONS_MANAGE"],
|
["permissions.ts", "PERMS.PERMISSIONS_MANAGE"],
|
||||||
|
["permissions.ts", "repairAdminNavAclGrants"],
|
||||||
["admin-guilds.ts", "PERMS.USERS_EDIT"],
|
["admin-guilds.ts", "PERMS.USERS_EDIT"],
|
||||||
["moderation.ts", "PERMS.MODERATION_EDIT"],
|
["moderation.ts", "PERMS.MODERATION_EDIT"],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export async function requireMod(): Promise<StaffUser> {
|
|||||||
canAccess(permissions, PERMS.MOD_BANS_VIEW, session.user.rank) ||
|
canAccess(permissions, PERMS.MOD_BANS_VIEW, session.user.rank) ||
|
||||||
canAccess(permissions, PERMS.MOD_TICKETS_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_TEAM_VIEW, session.user.rank) ||
|
||||||
|
canAccess(permissions, PERMS.MOD_USERS_VIEW, session.user.rank) ||
|
||||||
canAccess(permissions, PERMS.TICKETS_VIEW, session.user.rank);
|
canAccess(permissions, PERMS.TICKETS_VIEW, session.user.rank);
|
||||||
if (!ok) redirectSafe("/", "/");
|
if (!ok) redirectSafe("/", "/");
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
@@ -3010,8 +3010,19 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "CMS tickets",
|
"tickets": "CMS tickets",
|
||||||
"helpTickets": "Help-center tickets",
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users",
|
||||||
"team": "Team"
|
"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": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
"bansToday": "Bans today",
|
"bansToday": "Bans today",
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
@@ -3017,8 +3017,19 @@
|
|||||||
"backToSite": "Torna al sito",
|
"backToSite": "Torna al sito",
|
||||||
"tickets": "Ticket CMS",
|
"tickets": "Ticket CMS",
|
||||||
"helpTickets": "Ticket help center",
|
"helpTickets": "Ticket help center",
|
||||||
|
"users": "Utenti",
|
||||||
"team": "Team"
|
"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": {
|
"stats": {
|
||||||
"openCfh": "CFH aperti",
|
"openCfh": "CFH aperti",
|
||||||
"bansToday": "Ban oggi",
|
"bansToday": "Ban oggi",
|
||||||
|
|||||||
@@ -3015,8 +3015,19 @@
|
|||||||
"backToSite": "Terug naar site",
|
"backToSite": "Terug naar site",
|
||||||
"tickets": "CMS-tickets",
|
"tickets": "CMS-tickets",
|
||||||
"helpTickets": "Helpcenter-tickets",
|
"helpTickets": "Helpcenter-tickets",
|
||||||
|
"users": "Gebruikers",
|
||||||
"team": "Team"
|
"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": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
"bansToday": "Bans vandaag",
|
"bansToday": "Bans vandaag",
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3006,7 +3006,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3018,7 +3019,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
+13
-2
@@ -3008,7 +3008,8 @@
|
|||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"helpTickets": "Help-center tickets"
|
"helpTickets": "Help-center tickets",
|
||||||
|
"users": "Users"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3020,7 +3021,17 @@
|
|||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload",
|
"teamHint": "Online staff & workload",
|
||||||
"openHelpTickets": "Open help tickets"
|
"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": {
|
"emails": {
|
||||||
|
|||||||
Reference in new issue
Block a user