feat: improve users page with inline staff actions and commandocentrum cleanup

This commit is contained in:
remco committed 2026-07-01 18:38:50 +02:00
1 parent d588ae5f0f
commit beb36d2dbf
3 files changed
+298 -7

No files matched your search

+117 -6
View File
@@ -4,6 +4,17 @@ import {
updateCatalog,
updateNavigator,
updateWordFilter,
disconnectUser,
alertUser,
forwardUser,
giveCredits,
giveDuckets,
giveDiamonds,
giveBadge,
setMotto,
setRank,
executeCommand,
sendGift,
} from "@/actions/commandocentrum";
import { DiagnosticRow, InfoItem, StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
@@ -21,8 +32,6 @@ function fromDate(d: Date | null | undefined): string {
return d ? d.toISOString().slice(0, 19).replace("T", " ") : "—";
}
// stacktrace is a MySQL BLOB (Prisma `Bytes`), so it arrives as a Buffer/
// Uint8Array. Decode to UTF-8 and trim to a single readable preview line.
function decodeStacktrace(raw: unknown): string {
if (raw == null) return "";
try {
@@ -45,8 +54,6 @@ function uptime(seconds: number): string {
}
export default async function CommandoCentrum() {
// Gather everything in parallel; any failure degrades to a safe default so the
// panel still renders (and the DB-down state is itself a diagnostic).
const [onlineCount, totalUsers, errors, dbOk, emulatorOnline, maintenance, activity] =
await Promise.all([
prisma.user.count({ where: { online: "1" } }).catch(() => 0),
@@ -63,7 +70,6 @@ export default async function CommandoCentrum() {
.catch(() => [] as Awaited<ReturnType<typeof prisma.staffActivities.findMany>>),
]);
// Resolve staff usernames for the activity feed.
const staffIds = Array.from(new Set(activity.map((a) => Number(a.userId)))).filter((n) => n > 0);
let nameById = new Map<number, string>();
if (staffIds.length) {
@@ -224,6 +230,111 @@ export default async function CommandoCentrum() {
</form>
</section>
{/* ── User RCON actions ──────────────────────────────────── */}
<section className="admin-section">
<h2>User actions (RCON)</h2>
<p className="muted" style={{ marginTop: "-0.4rem" }}>
Enter a User ID to perform actions on a specific user. Username required for disconnect.
</p>
<div className="grid cols-3">
<form action={disconnectUser} className="card">
<h3 style={{ marginTop: 0 }}>Disconnect / Kick</h3>
<p className="muted" style={{ margin: "0 0 0.75rem" }}>Force disconnect a user.</p>
<input name="userId" type="number" min={1} placeholder="User ID" required style={{ width: "100%", marginBottom: "0.5rem" }} />
<input name="username" type="text" placeholder="Username (required)" required style={{ width: "100%", marginBottom: "0.75rem" }} />
<button type="submit" className="btn btn-danger">Disconnect user</button>
</form>
<form action={alertUser} className="card">
<h3 style={{ marginTop: 0 }}>Alert user</h3>
<p className="muted" style={{ margin: "0 0 0.75rem" }}>Send a popup alert to a user.</p>
<input name="userId" type="number" min={1} placeholder="User ID" required style={{ width: "100%", marginBottom: "0.5rem" }} />
<input name="message" type="text" placeholder="Alert message" required maxLength={512} style={{ width: "100%", marginBottom: "0.75rem" }} />
<button type="submit" className="btn btn-primary">Send alert</button>
</form>
<form action={forwardUser} className="card">
<h3 style={{ marginTop: 0 }}>Forward to room</h3>
<p className="muted" style={{ margin: "0 0 0.75rem" }}>Teleport user to a room ID.</p>
<input name="userId" type="number" min={1} placeholder="User ID" required style={{ width: "100%", marginBottom: "0.5rem" }} />
<input name="roomId" type="number" min={1} placeholder="Room ID" required style={{ width: "100%", marginBottom: "0.75rem" }} />
<button type="submit" className="btn btn-primary">Forward user</button>
</form>
</div>
<div className="grid cols-3" style={{ marginTop: "1rem" }}>
<form action={giveCredits} className="card">
<h3 style={{ marginTop: 0 }}>Give credits</h3>
<p className="muted" style={{ margin: "0 0 0.75rem" }}>Add credits to a user.</p>
<input name="userId" type="number" min={1} placeholder="User ID" required style={{ width: "100%", marginBottom: "0.5rem" }} />
<input name="credits" type="number" min={1} placeholder="Amount" required style={{ width: "100%", marginBottom: "0.75rem" }} />
<button type="submit" className="btn btn-primary">Give credits</button>
</form>
<form action={giveDuckets} className="card">
<h3 style={{ marginTop: 0 }}>Give duckets</h3>
<p className="muted" style={{ margin: "0 0 0.75rem" }}>Add duckets to a user.</p>
<input name="userId" type="number" min={1} placeholder="User ID" required style={{ width: "100%", marginBottom: "0.5rem" }} />
<input name="amount" type="number" min={1} placeholder="Amount" required style={{ width: "100%", marginBottom: "0.75rem" }} />
<button type="submit" className="btn btn-primary">Give duckets</button>
</form>
<form action={giveDiamonds} className="card">
<h3 style={{ marginTop: 0 }}>Give diamonds</h3>
<p className="muted" style={{ margin: "0 0 0.75rem" }}>Add diamonds to a user.</p>
<input name="userId" type="number" min={1} placeholder="User ID" required style={{ width: "100%", marginBottom: "0.5rem" }} />
<input name="amount" type="number" min={1} placeholder="Amount" required style={{ width: "100%", marginBottom: "0.75rem" }} />
<button type="submit" className="btn btn-primary">Give diamonds</button>
</form>
</div>
<div className="grid cols-3" style={{ marginTop: "1rem" }}>
<form action={giveBadge} className="card">
<h3 style={{ marginTop: 0 }}>Give badge</h3>
<p className="muted" style={{ margin: "0 0 0.75rem" }}>Give a badge code (e.g. HOF1).</p>
<input name="userId" type="number" min={1} placeholder="User ID" required style={{ width: "100%", marginBottom: "0.5rem" }} />
<input name="badge" type="text" placeholder="Badge code" required maxLength={32} style={{ width: "100%", marginBottom: "0.75rem" }} />
<button type="submit" className="btn btn-primary">Give badge</button>
</form>
<form action={setMotto} className="card">
<h3 style={{ marginTop: 0 }}>Set motto</h3>
<p className="muted" style={{ margin: "0 0 0.75rem" }}>Change user's motto (max 127 chars).</p>
<input name="userId" type="number" min={1} placeholder="User ID" required style={{ width: "100%", marginBottom: "0.5rem" }} />
<input name="motto" type="text" placeholder="New motto" required maxLength={127} style={{ width: "100%", marginBottom: "0.75rem" }} />
<button type="submit" className="btn btn-primary">Set motto</button>
</form>
<form action={setRank} className="card">
<h3 style={{ marginTop: 0 }}>Set rank</h3>
<p className="muted" style={{ margin: "0 0 0.75rem" }}>Change user rank (0–10).</p>
<input name="userId" type="number" min={1} placeholder="User ID" required style={{ width: "100%", marginBottom: "0.5rem" }} />
<input name="rank" type="number" min={0} max={10} placeholder="Rank (0–10)" required style={{ width: "100%", marginBottom: "0.75rem" }} />
<button type="submit" className="btn btn-primary">Set rank</button>
</form>
</div>
<div className="grid cols-3" style={{ marginTop: "1rem" }}>
<form action={executeCommand} className="card">
<h3 style={{ marginTop: 0 }}>Execute command</h3>
<p className="muted" style={{ margin: "0 0 0.75rem" }}>Run a chat command as the user.</p>
<input name="userId" type="number" min={1} placeholder="User ID" required style={{ width: "100%", marginBottom: "0.5rem" }} />
<input name="command" type="text" placeholder="Command (e.g. :pickall)" required maxLength={100} style={{ width: "100%", marginBottom: "0.75rem" }} />
<button type="submit" className="btn btn-primary">Execute</button>
</form>
<form action={sendGift} className="card">
<h3 style={{ marginTop: 0 }}>Send gift</h3>
<p className="muted" style={{ margin: "0 0 0.75rem" }}>Send a furniture item as gift.</p>
<input name="userId" type="number" min={1} placeholder="User ID" required style={{ width: "100%", marginBottom: "0.5rem" }} />
<input name="itemId" type="number" min={1} placeholder="Furni ID" required style={{ width: "100%", marginBottom: "0.5rem" }} />
<input name="message" type="text" placeholder="Gift message" maxLength={255} defaultValue="Here is a gift." style={{ width: "100%", marginBottom: "0.75rem" }} />
<button type="submit" className="btn btn-primary">Send gift</button>
</form>
</div>
</section>
{/* ── Staff activity ─────────────────────────────────────── */}
<section className="admin-section">
<h2>Recent staff activity</h2>
@@ -309,4 +420,4 @@ export default async function CommandoCentrum() {
</section>
</main>
);
}
}