aboutsummaryrefslogtreecommitdiffstats
path: root/packages/web/server/api
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-03-01 23:17:27 +0000
committerMohamedBassem <me@mbassem.com>2024-03-01 23:19:03 +0000
commit7ddcfb630d3dec3d9fecbfd6a498ca7c572809ec (patch)
treec0339e9181b35d0819bc0bfd1219ccdb262e54d2 /packages/web/server/api
parenta5434730ede1272f195d6a4b13207b840a5ac2cf (diff)
downloadkarakeep-7ddcfb630d3dec3d9fecbfd6a498ca7c572809ec.tar.zst
feature: Add an admin page showing server stats and actions
Diffstat (limited to 'packages/web/server/api')
-rw-r--r--packages/web/server/api/routers/_app.ts2
-rw-r--r--packages/web/server/api/routers/admin.ts86
2 files changed, 88 insertions, 0 deletions
diff --git a/packages/web/server/api/routers/_app.ts b/packages/web/server/api/routers/_app.ts
index 6a1b05e9..43ab6f5d 100644
--- a/packages/web/server/api/routers/_app.ts
+++ b/packages/web/server/api/routers/_app.ts
@@ -1,4 +1,5 @@
import { router } from "../trpc";
+import { adminAppRouter } from "./admin";
import { apiKeysAppRouter } from "./apiKeys";
import { bookmarksAppRouter } from "./bookmarks";
import { listsAppRouter } from "./lists";
@@ -8,6 +9,7 @@ export const appRouter = router({
apiKeys: apiKeysAppRouter,
users: usersAppRouter,
lists: listsAppRouter,
+ admin: adminAppRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;
diff --git a/packages/web/server/api/routers/admin.ts b/packages/web/server/api/routers/admin.ts
new file mode 100644
index 00000000..92769151
--- /dev/null
+++ b/packages/web/server/api/routers/admin.ts
@@ -0,0 +1,86 @@
+import { authedProcedure, router } from "../trpc";
+import { z } from "zod";
+import { TRPCError } from "@trpc/server";
+import { count } from "drizzle-orm";
+import { bookmarks, users } from "@hoarder/db/schema";
+import {
+ LinkCrawlerQueue,
+ OpenAIQueue,
+ SearchIndexingQueue,
+} from "@hoarder/shared/queues";
+
+const adminProcedure = authedProcedure.use(function isAdmin(opts) {
+ const user = opts.ctx.user;
+ if (user.role != "admin") {
+ throw new TRPCError({ code: "FORBIDDEN" });
+ }
+ return opts.next(opts);
+});
+
+export const adminAppRouter = router({
+ stats: adminProcedure
+ .output(
+ z.object({
+ numUsers: z.number(),
+ numBookmarks: z.number(),
+ pendingCrawls: z.number(),
+ pendingIndexing: z.number(),
+ pendingOpenai: z.number(),
+ }),
+ )
+ .query(async ({ ctx }) => {
+ const [
+ [{ value: numUsers }],
+ [{ value: numBookmarks }],
+ pendingCrawls,
+ pendingIndexing,
+ pendingOpenai,
+ ] = await Promise.all([
+ ctx.db.select({ value: count() }).from(users),
+ ctx.db.select({ value: count() }).from(bookmarks),
+ LinkCrawlerQueue.getWaitingCount(),
+ SearchIndexingQueue.getWaitingCount(),
+ OpenAIQueue.getWaitingCount(),
+ ]);
+
+ return {
+ numUsers,
+ numBookmarks,
+ pendingCrawls,
+ pendingIndexing,
+ pendingOpenai,
+ };
+ }),
+ recrawlAllLinks: adminProcedure.mutation(async ({ ctx }) => {
+ const bookmarkIds = await ctx.db.query.bookmarkLinks.findMany({
+ columns: {
+ id: true,
+ },
+ });
+
+ await Promise.all(
+ bookmarkIds.map((b) =>
+ LinkCrawlerQueue.add("crawl", {
+ bookmarkId: b.id,
+ }),
+ ),
+ );
+ }),
+
+ reindexAllBookmarks: adminProcedure.mutation(async ({ ctx }) => {
+ const bookmarkIds = await ctx.db.query.bookmarks.findMany({
+ columns: {
+ id: true,
+ },
+ });
+
+ await Promise.all(
+ bookmarkIds.map((b) =>
+ SearchIndexingQueue.add("search_indexing", {
+ bookmarkId: b.id,
+ type: "index",
+ }),
+ ),
+ );
+ }),
+});