From 0f9132b5a9186accd991492b73b9ef904342df29 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 11 Jan 2026 09:27:35 +0000 Subject: feat: privacy-respecting bookmark debugger admin tool (#2373) * fix: parallelize queue enqueues in bookmark routes * fix: guard meilisearch client init with mutex * feat: add bookmark debugging admin tool * more fixes * more fixes * more fixes --- apps/web/components/admin/BookmarkDebugger.tsx | 649 +++++++++++++++++++++++++ apps/web/components/ui/info-tooltip.tsx | 3 +- 2 files changed, 650 insertions(+), 2 deletions(-) create mode 100644 apps/web/components/admin/BookmarkDebugger.tsx (limited to 'apps/web/components') diff --git a/apps/web/components/admin/BookmarkDebugger.tsx b/apps/web/components/admin/BookmarkDebugger.tsx new file mode 100644 index 00000000..1628fdcc --- /dev/null +++ b/apps/web/components/admin/BookmarkDebugger.tsx @@ -0,0 +1,649 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { AdminCard } from "@/components/admin/AdminCard"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import InfoTooltip from "@/components/ui/info-tooltip"; +import { Input } from "@/components/ui/input"; +import { useTranslation } from "@/lib/i18n/client"; +import { api } from "@/lib/trpc"; +import { formatBytes } from "@/lib/utils"; +import { formatDistanceToNow } from "date-fns"; +import { + AlertCircle, + CheckCircle2, + ChevronDown, + ChevronRight, + Clock, + Database, + ExternalLink, + FileText, + FileType, + Image as ImageIcon, + Link as LinkIcon, + Loader2, + RefreshCw, + Search, + Sparkles, + Tag, + User, + XCircle, +} from "lucide-react"; +import { parseAsString, useQueryState } from "nuqs"; +import { toast } from "sonner"; + +import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; + +export default function BookmarkDebugger() { + const { t } = useTranslation(); + const [inputValue, setInputValue] = useState(""); + const [bookmarkId, setBookmarkId] = useQueryState( + "bookmarkId", + parseAsString.withDefault(""), + ); + const [showHtmlPreview, setShowHtmlPreview] = useState(false); + + // Sync input value with URL on mount/change + useEffect(() => { + if (bookmarkId) { + setInputValue(bookmarkId); + } + }, [bookmarkId]); + + const { + data: debugInfo, + isLoading, + error, + } = api.admin.getBookmarkDebugInfo.useQuery( + { bookmarkId: bookmarkId }, + { enabled: !!bookmarkId && bookmarkId.length > 0 }, + ); + + const handleLookup = () => { + if (inputValue.trim()) { + setBookmarkId(inputValue.trim()); + } + }; + + const recrawlMutation = api.admin.adminRecrawlBookmark.useMutation({ + onSuccess: () => { + toast.success(t("admin.admin_tools.action_success"), { + description: t("admin.admin_tools.recrawl_queued"), + }); + }, + onError: (error) => { + toast.error(t("admin.admin_tools.action_failed"), { + description: error.message, + }); + }, + }); + + const reindexMutation = api.admin.adminReindexBookmark.useMutation({ + onSuccess: () => { + toast.success(t("admin.admin_tools.action_success"), { + description: t("admin.admin_tools.reindex_queued"), + }); + }, + onError: (error) => { + toast.error(t("admin.admin_tools.action_failed"), { + description: error.message, + }); + }, + }); + + const retagMutation = api.admin.adminRetagBookmark.useMutation({ + onSuccess: () => { + toast.success(t("admin.admin_tools.action_success"), { + description: t("admin.admin_tools.retag_queued"), + }); + }, + onError: (error) => { + toast.error(t("admin.admin_tools.action_failed"), { + description: error.message, + }); + }, + }); + + const resummarizeMutation = api.admin.adminResummarizeBookmark.useMutation({ + onSuccess: () => { + toast.success(t("admin.admin_tools.action_success"), { + description: t("admin.admin_tools.resummarize_queued"), + }); + }, + onError: (error) => { + toast.error(t("admin.admin_tools.action_failed"), { + description: error.message, + }); + }, + }); + + const handleRecrawl = () => { + if (bookmarkId) { + recrawlMutation.mutate({ bookmarkId }); + } + }; + + const handleReindex = () => { + if (bookmarkId) { + reindexMutation.mutate({ bookmarkId }); + } + }; + + const handleRetag = () => { + if (bookmarkId) { + retagMutation.mutate({ bookmarkId }); + } + }; + + const handleResummarize = () => { + if (bookmarkId) { + resummarizeMutation.mutate({ bookmarkId }); + } + }; + + const getStatusBadge = (status: "pending" | "failure" | "success" | null) => { + if (!status) return null; + + const config = { + success: { + variant: "default" as const, + icon: CheckCircle2, + }, + failure: { + variant: "destructive" as const, + icon: XCircle, + }, + pending: { + variant: "secondary" as const, + icon: AlertCircle, + }, + }; + + const { variant, icon: Icon } = config[status]; + + return ( + + + {status} + + ); + }; + + return ( +
+ {/* Input Section */} + +
+ +

+ {t("admin.admin_tools.bookmark_debugger")} +

+ + Some data will be redacted for privacy. + +
+
+
+ + setInputValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleLookup(); + } + }} + className="pl-9" + /> +
+ +
+
+ + {/* Loading State */} + {isLoading && ( + +
+ +
+
+ )} + + {/* Error State */} + {!isLoading && error && ( + +
+ +
+

+ {t("admin.admin_tools.fetch_error")} +

+

+ {error.message} +

+
+
+
+ )} + + {/* Debug Info Display */} + {!isLoading && !error && debugInfo && ( + +
+ {/* Basic Info & Status */} +
+ {/* Basic Info */} +
+
+ +

+ {t("admin.admin_tools.basic_info")} +

+
+
+
+ + + {t("common.id")} + + {debugInfo.id} +
+
+ + + {t("common.type")} + + {debugInfo.type} +
+
+ + + {t("common.source")} + + {debugInfo.source || "N/A"} +
+
+ + + {t("admin.admin_tools.owner_user_id")} + + + {debugInfo.userId} + +
+
+ + + {t("common.created_at")} + + + {formatDistanceToNow(new Date(debugInfo.createdAt), { + addSuffix: true, + })} + +
+ {debugInfo.modifiedAt && ( +
+ + + {t("common.updated_at")} + + + {formatDistanceToNow(new Date(debugInfo.modifiedAt), { + addSuffix: true, + })} + +
+ )} +
+
+ + {/* Status */} +
+
+ +

+ {t("admin.admin_tools.status")} +

+
+
+
+ + + {t("admin.admin_tools.tagging_status")} + + {getStatusBadge(debugInfo.taggingStatus)} +
+
+ + + {t("admin.admin_tools.summarization_status")} + + {getStatusBadge(debugInfo.summarizationStatus)} +
+ {debugInfo.linkInfo && ( + <> +
+ + + {t("admin.admin_tools.crawl_status")} + + {getStatusBadge(debugInfo.linkInfo.crawlStatus)} +
+
+ + + {t("admin.admin_tools.crawl_status_code")} + + = 200 && + debugInfo.linkInfo.crawlStatusCode < 300) + ? "default" + : "destructive" + } + > + {debugInfo.linkInfo.crawlStatusCode} + +
+ {debugInfo.linkInfo.crawledAt && ( +
+ + + {t("admin.admin_tools.crawled_at")} + + + {formatDistanceToNow( + new Date(debugInfo.linkInfo.crawledAt), + { + addSuffix: true, + }, + )} + +
+ )} + + )} +
+
+
+ + {/* Content */} + {(debugInfo.title || + debugInfo.summary || + debugInfo.linkInfo || + debugInfo.textInfo?.sourceUrl || + debugInfo.assetInfo) && ( +
+
+ +

+ {t("admin.admin_tools.content")} +

+
+
+ {debugInfo.title && ( +
+
+ + {t("common.title")} +
+
+ {debugInfo.title} +
+
+ )} + {debugInfo.summary && ( +
+
+ + {t("admin.admin_tools.summary")} +
+
+ {debugInfo.summary} +
+
+ )} + {debugInfo.linkInfo && ( +
+
+ + {t("admin.admin_tools.url")} +
+ + + {debugInfo.linkInfo.url} + + + +
+ )} + {debugInfo.textInfo?.sourceUrl && ( +
+
+ + {t("admin.admin_tools.source_url")} +
+ + + {debugInfo.textInfo.sourceUrl} + + + +
+ )} + {debugInfo.assetInfo && ( +
+
+ + {t("admin.admin_tools.asset_type")} +
+
+ + {debugInfo.assetInfo.assetType} + + {debugInfo.assetInfo.fileName && ( +
+ {debugInfo.assetInfo.fileName} +
+ )} +
+
+ )} +
+
+ )} + + {/* HTML Preview */} + {debugInfo.linkInfo && debugInfo.linkInfo.htmlContentPreview && ( +
+ + {showHtmlPreview && ( +
+                    {debugInfo.linkInfo.htmlContentPreview}
+                  
+ )} +
+ )} + + {/* Tags */} + {debugInfo.tags.length > 0 && ( +
+
+ +

+ {t("common.tags")}{" "} + + ({debugInfo.tags.length}) + +

+
+
+ {debugInfo.tags.map((tag) => ( + + {tag.attachedBy === "ai" && ( + + )} + {tag.name} + + ))} +
+
+ )} + + {/* Assets */} + {debugInfo.assets.length > 0 && ( +
+
+ +

+ {t("common.attachments")}{" "} + + ({debugInfo.assets.length}) + +

+
+
+ {debugInfo.assets.map((asset) => ( +
+
+ +
+ + {asset.assetType} + +
+ {formatBytes(asset.size)} +
+
+
+ {asset.url && ( + + {t("admin.admin_tools.view")} + + + )} +
+ ))} +
+
+ )} + + {/* Actions */} +
+
+ +

{t("common.actions")}

+
+
+ + + + +
+
+
+
+ )} +
+ ); +} diff --git a/apps/web/components/ui/info-tooltip.tsx b/apps/web/components/ui/info-tooltip.tsx index 4dd97199..9d525983 100644 --- a/apps/web/components/ui/info-tooltip.tsx +++ b/apps/web/components/ui/info-tooltip.tsx @@ -22,8 +22,7 @@ export default function InfoTooltip({ {variant === "tip" ? ( ) : ( -- cgit v1.2.3-70-g09d2