diff --git a/app/panel/inspect/account/[id]/page.tsx b/app/panel/inspect/account/[id]/page.tsx
new file mode 100644
index 0000000..29697a6
--- /dev/null
+++ b/app/panel/inspect/account/[id]/page.tsx
@@ -0,0 +1,26 @@
+import { UserCard } from "@/components/cards/UserCard";
+import { NavigationToolbar } from "@/components/common/NavigationToolbar";
+import { fetchUserById } from "@/lib/db";
+import { notFound } from "next/navigation";
+
+export default async function User({
+ params,
+}: {
+ params: { id: string; type: string };
+}) {
+ const user = await fetchUserById(params.id);
+ // if (!user) return notFound();
+
+ return (
+
+ Inspecting Account
+ {user && }
+ update password, reset 2FA, disable / undisable (disabled if pending
+ delete), delete / cancel delete
+
+ list active 2FA methods without keys
+
+ email account
+
+ );
+}
diff --git a/app/panel/inspect/channel/[id]/page.tsx b/app/panel/inspect/channel/[id]/page.tsx
new file mode 100644
index 0000000..6e4bdd8
--- /dev/null
+++ b/app/panel/inspect/channel/[id]/page.tsx
@@ -0,0 +1,25 @@
+import { ChannelCard } from "@/components/cards/ChannelCard";
+import { JsonCard } from "@/components/cards/JsonCard";
+import { ServerCard } from "@/components/cards/ServerCard";
+import { NavigationToolbar } from "@/components/common/NavigationToolbar";
+import { fetchChannelById, fetchServerById } from "@/lib/db";
+import { notFound } from "next/navigation";
+
+export default async function Message({ params }: { params: { id: string } }) {
+ const channel = await fetchChannelById(params.id);
+ if (!channel) return notFound();
+
+ const server =
+ channel.channel_type === "TextChannel"
+ ? await fetchServerById(channel.server)
+ : undefined;
+
+ return (
+
+ Inspecting Channel
+
+ {server && }
+
+
+ );
+}
diff --git a/app/panel/inspect/message/[id]/page.tsx b/app/panel/inspect/message/[id]/page.tsx
new file mode 100644
index 0000000..450fa7a
--- /dev/null
+++ b/app/panel/inspect/message/[id]/page.tsx
@@ -0,0 +1,40 @@
+import { ChannelCard } from "@/components/cards/ChannelCard";
+import { JsonCard } from "@/components/cards/JsonCard";
+import { UserCard } from "@/components/cards/UserCard";
+import { NavigationToolbar } from "@/components/common/NavigationToolbar";
+import { Card, CardHeader } from "@/components/ui/card";
+import { fetchChannelById, fetchMessageById, fetchUserById } from "@/lib/db";
+import Link from "next/link";
+import { notFound } from "next/navigation";
+
+export default async function Message({
+ params,
+}: {
+ params: { id: string; type: string };
+}) {
+ const message = await fetchMessageById(params.id);
+ if (!message) return notFound();
+
+ const author = await fetchUserById(message.author);
+ const channel = await fetchChannelById(message.channel);
+
+ return (
+
+
Inspecting Message
+
+
+
+ {message.content}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/panel/inspect/page.tsx b/app/panel/inspect/page.tsx
new file mode 100644
index 0000000..3163663
--- /dev/null
+++ b/app/panel/inspect/page.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+
+export default function Inspect() {
+ const [id, setId] = useState("");
+ const router = useRouter();
+
+ const createHandler = (type: string) => () =>
+ router.push(`/panel/inspect/${type}/${id}`);
+
+ return (
+
+
setId(e.currentTarget.value)}
+ />
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/panel/inspect/server/[id]/page.tsx b/app/panel/inspect/server/[id]/page.tsx
new file mode 100644
index 0000000..cda7726
--- /dev/null
+++ b/app/panel/inspect/server/[id]/page.tsx
@@ -0,0 +1,18 @@
+import { JsonCard } from "@/components/cards/JsonCard";
+import { ServerCard } from "@/components/cards/ServerCard";
+import { NavigationToolbar } from "@/components/common/NavigationToolbar";
+import { fetchServerById } from "@/lib/db";
+import { notFound } from "next/navigation";
+
+export default async function Server({ params }: { params: { id: string } }) {
+ const server = await fetchServerById(params.id);
+ if (!server) return notFound();
+
+ return (
+
+ Inspecting Server
+
+
+
+ );
+}
diff --git a/app/panel/inspect/user/[id]/page.tsx b/app/panel/inspect/user/[id]/page.tsx
new file mode 100644
index 0000000..40e4b0c
--- /dev/null
+++ b/app/panel/inspect/user/[id]/page.tsx
@@ -0,0 +1,121 @@
+import { JsonCard } from "@/components/cards/JsonCard";
+import { UserCard } from "@/components/cards/UserCard";
+import { NavigationToolbar } from "@/components/common/NavigationToolbar";
+import { RelevantModerationNotices } from "@/components/inspector/RelevantModerationNotices";
+import { RelevantObjects } from "@/components/inspector/RelevantObjects";
+import { RelevantReports } from "@/components/inspector/RelevantReports";
+import { UserActions } from "@/components/inspector/UserActions";
+import { Card, CardHeader } from "@/components/ui/card";
+import { Separator } from "@/components/ui/separator";
+import { PLATFORM_MOD_ID } from "@/lib/constants";
+import {
+ fetchBotsByUser,
+ fetchMembershipsByUser,
+ fetchMessages,
+ fetchReports,
+ fetchServers,
+ fetchSnapshots,
+ fetchStrikesByUser,
+ fetchUserById,
+ fetchUsersById,
+ findDM,
+} from "@/lib/db";
+import Link from "next/link";
+import { notFound } from "next/navigation";
+
+export default async function User({
+ params,
+}: {
+ params: { id: string; type: string };
+}) {
+ const user = await fetchUserById(params.id);
+ if (!user) return notFound();
+
+ const botOwner = user.bot ? await fetchUserById(user.bot.owner) : undefined;
+
+ // Fetch strikes
+ const strikes = await fetchStrikesByUser(user._id);
+
+ // Fetch moderation alerts
+ const dm = await findDM(PLATFORM_MOD_ID, user._id);
+ const notices = dm ? await fetchMessages({ channel: dm._id }) : [];
+
+ // Fetch friends and bots
+ const botIds = await fetchBotsByUser(user._id).then((bots) =>
+ bots.map((bot) => bot._id)
+ );
+
+ const relevantUsers = await fetchUsersById([
+ ...botIds,
+ ...(
+ user.relations?.filter((relation) => relation.status === "Friend") ?? []
+ ).map((relation) => relation._id),
+ ]);
+
+ relevantUsers.sort((a) => (a.bot ? -1 : 0));
+
+ // Fetch server memberships
+ const serverMemberships = await fetchMembershipsByUser(user._id);
+ const servers = await fetchServers({
+ _id: {
+ $in: serverMemberships.map((member) => member._id.server),
+ },
+ });
+
+ // Fetch reports
+ const reportsByUser = await fetchReports({
+ author_id: user._id,
+ });
+
+ const reportIdsInvolvingUser = await fetchSnapshots({
+ // TODO: slow query
+ $or: [{ "content._id": user._id }, { "content.author": user._id }],
+ }).then((snapshots) => [
+ ...new Set(snapshots.map((snapshot) => snapshot.report_id)),
+ ]);
+
+ const reportsAgainstUser = await fetchReports({
+ _id: {
+ $in: reportIdsInvolvingUser,
+ },
+ });
+
+ return (
+
+
Inspecting User
+
+
+
+
+ {user.profile?.content && (
+
+
+ {user.profile?.content}
+
+
+ )}
+
+ {botOwner && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}