"use client"; import { useRouter } from "next/navigation"; import { ActionButton } from "@/components/ui/action-button"; import { Separator } from "@/components/ui/separator"; import LoadingSpinner from "@/components/ui/spinner"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { toast } from "@/components/ui/use-toast"; import { useClientConfig } from "@/lib/clientConfig"; import { api } from "@/lib/trpc"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { Trash } from "lucide-react"; import { useSession } from "next-auth/react"; const REPO_LATEST_RELEASE_API = "https://api.github.com/repos/hoarder-app/hoarder/releases/latest"; const REPO_RELEASE_PAGE = "https://github.com/hoarder-app/hoarder/releases"; function useLatestRelease() { const { data } = useQuery({ queryKey: ["latest-release"], queryFn: async () => { const res = await fetch(REPO_LATEST_RELEASE_API); if (!res.ok) { return undefined; } const data = (await res.json()) as { name: string }; return data.name; }, staleTime: 60 * 60 * 1000, enabled: !useClientConfig().disableNewReleaseCheck, }); return data; } function ReleaseInfo() { const currentRelease = useClientConfig().serverVersion ?? "not set"; const latestRelease = useLatestRelease(); let newRelease; if (latestRelease && currentRelease != latestRelease) { newRelease = ( (New release available: {latestRelease}) ); } return (

{currentRelease} {newRelease}

); } function ActionsSection() { const { mutate: recrawlLinks, isPending: isRecrawlPending } = api.admin.recrawlLinks.useMutation({ onSuccess: () => { toast({ description: "Recrawl enqueued", }); }, onError: (e) => { toast({ variant: "destructive", description: e.message, }); }, }); const { mutate: reindexBookmarks, isPending: isReindexPending } = api.admin.reindexAllBookmarks.useMutation({ onSuccess: () => { toast({ description: "Reindex enqueued", }); }, onError: (e) => { toast({ variant: "destructive", description: e.message, }); }, }); return ( <>

Actions

recrawlLinks({ crawlStatus: "failure", runInference: true }) } > Recrawl Failed Links Only recrawlLinks({ crawlStatus: "all", runInference: true })} > Recrawl All Links recrawlLinks({ crawlStatus: "all", runInference: false }) } > Recrawl All Links (Without Inference) reindexBookmarks()} > Reindex All Bookmarks ); } function ServerStatsSection() { const { data: serverStats } = api.admin.stats.useQuery(undefined, { refetchInterval: 1000, placeholderData: keepPreviousData, }); if (!serverStats) { return ; } return ( <>

Server Stats

Num Users {serverStats.numUsers} Num Bookmarks {serverStats.numBookmarks} Server Version

Background Jobs

Job Queued Pending Failed Crawling Jobs {serverStats.crawlStats.queuedInRedis} {serverStats.crawlStats.pending} {serverStats.crawlStats.failed} Indexing Jobs {serverStats.indexingStats.queuedInRedis} - - Inference Jobs {serverStats.inferenceStats.queuedInRedis} {serverStats.inferenceStats.pending} {serverStats.inferenceStats.failed}
); } function UsersSection() { const { data: session } = useSession(); const invalidateUserList = api.useUtils().users.list.invalidate; const { data: users } = api.users.list.useQuery(); const { mutate: deleteUser, isPending: isDeletionPending } = api.users.delete.useMutation({ onSuccess: () => { toast({ description: "User deleted", }); invalidateUserList(); }, onError: (e) => { toast({ variant: "destructive", description: `Something went wrong: ${e.message}`, }); }, }); if (!users) { return ; } return ( <>

Users

Name Email Role Action {users.users.map((u) => ( {u.name} {u.email} {u.role} deleteUser({ userId: u.id })} loading={isDeletionPending} disabled={session!.user.id == u.id} > ))}
); } export default function AdminPage() { const router = useRouter(); const { data: session, status } = useSession(); if (status == "loading") { return ; } if (!session || session.user.role != "admin") { router.push("/"); return; } return (

Admin

); }