feat: implement debug mode with enhanced logging and filtering capabilities

This commit is contained in:
Julien Froidefond
2025-10-07 21:08:20 +02:00
parent 760bd14aa7
commit df6a30b226
9 changed files with 420 additions and 60 deletions

View File

@@ -1,5 +1,5 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { usePathname } from "next/navigation";
import {
X,
@@ -11,9 +11,12 @@ import {
Layout,
RefreshCw,
Globe,
Filter,
Calendar,
} from "lucide-react";
import type { CacheType } from "@/lib/services/base-api.service";
import { useTranslation } from "react-i18next";
import { useDebug } from "@/contexts/DebugContext";
interface RequestTiming {
url: string;
@@ -45,10 +48,13 @@ function formatDuration(duration: number) {
return Math.round(duration);
}
type FilterType = "all" | "current-page" | "api" | "cache" | "mongodb" | "page-render";
export function DebugInfo() {
const [logs, setLogs] = useState<RequestTiming[]>([]);
const { logs, setLogs, clearLogs: clearLogsContext, isRefreshing, setIsRefreshing } = useDebug();
const [isMinimized, setIsMinimized] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [filter, setFilter] = useState<FilterType>("all");
const [showFilters, setShowFilters] = useState(false);
const pathname = usePathname();
const { t } = useTranslation();
@@ -67,32 +73,46 @@ export function DebugInfo() {
}
};
// Rafraîchir les logs au montage et à chaque changement de page
useEffect(() => {
fetchLogs();
}, [pathname]);
// Rafraîchir les logs périodiquement si la fenêtre n'est pas minimisée
useEffect(() => {
if (isMinimized) return;
const interval = setInterval(() => {
fetchLogs();
}, 5000); // Rafraîchir toutes les 5 secondes
return () => clearInterval(interval);
}, [isMinimized]);
const clearLogs = async () => {
try {
await fetch("/api/debug", { method: "DELETE" });
setLogs([]);
clearLogsContext();
} catch (error) {
console.error("Erreur lors de la suppression des logs:", error);
}
};
const sortedLogs = [...logs].reverse();
// Fonction pour déterminer si une requête appartient à la page courante
const isCurrentPageRequest = (log: RequestTiming): boolean => {
if (log.pageRender) {
return log.pageRender.page === pathname;
}
// Pour les requêtes API, on considère qu'elles appartiennent à la page courante
// si elles ont été faites récemment (dans les 30 dernières secondes)
const logTime = new Date(log.timestamp).getTime();
const now = Date.now();
return now - logTime < 30000; // 30 secondes
};
// Filtrer les logs selon le filtre sélectionné
const filteredLogs = logs.filter((log) => {
switch (filter) {
case "current-page":
return isCurrentPageRequest(log);
case "api":
return !log.fromCache && !log.mongoAccess && !log.pageRender;
case "cache":
return log.fromCache;
case "mongodb":
return log.mongoAccess;
case "page-render":
return log.pageRender;
default:
return true;
}
});
const sortedLogs = [...filteredLogs].reverse();
return (
<div
@@ -110,6 +130,15 @@ export function DebugInfo() {
)}
</div>
<div className="flex items-center gap-2">
{!isMinimized && (
<button
onClick={() => setShowFilters(!showFilters)}
className={`hover:bg-zinc-700 rounded-full p-1.5 ${showFilters ? "bg-zinc-700" : ""}`}
aria-label="Filtres"
>
<Filter className="h-5 w-5" />
</button>
)}
<button
onClick={fetchLogs}
className="hover:bg-zinc-700 rounded-full p-1.5"
@@ -135,15 +164,52 @@ export function DebugInfo() {
</div>
</div>
{!isMinimized && showFilters && (
<div className="mb-4 p-3 bg-zinc-800 rounded-lg">
<div className="flex flex-wrap gap-2">
{[
{ key: "all", label: "Toutes", icon: Calendar },
{ key: "current-page", label: "Page courante", icon: Layout },
{ key: "api", label: "API", icon: Globe },
{ key: "cache", label: "Cache", icon: Database },
{ key: "mongodb", label: "MongoDB", icon: CircleDot },
{ key: "page-render", label: "Rendu", icon: Layout },
].map(({ key, label, icon: Icon }) => (
<button
key={key}
onClick={() => setFilter(key as FilterType)}
className={`flex items-center gap-1 px-3 py-1.5 rounded-full text-xs transition-colors ${
filter === key
? "bg-blue-600 text-white"
: "bg-zinc-700 text-zinc-300 hover:bg-zinc-600"
}`}
>
<Icon className="h-3 w-3" />
{label}
</button>
))}
</div>
</div>
)}
{!isMinimized && (
<div className="space-y-3">
{sortedLogs.length === 0 ? (
<p className="text-sm opacity-75">{t("debug.noRequests")}</p>
) : (
sortedLogs.map((log, index) => (
<div key={index} className="text-sm space-y-1.5 bg-zinc-800 p-2 rounded">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2 min-w-0 flex-1">
sortedLogs.map((log, index) => {
const isCurrentPage = isCurrentPageRequest(log);
return (
<div
key={index}
className={`text-sm space-y-1.5 p-2 rounded border-l-2 ${
isCurrentPage
? "bg-blue-900/20 border-blue-500"
: "bg-zinc-800 border-zinc-700"
}`}
>
<div className="flex justify-between items-center">
<div className="flex items-center gap-2 min-w-0 flex-1">
{log.fromCache && (
<div
title={t("debug.tooltips.cache", { type: log.cacheType || "DEFAULT" })}
@@ -231,7 +297,8 @@ export function DebugInfo() {
)}
</div>
</div>
))
);
})
)}
</div>
)}

View File

@@ -10,6 +10,7 @@ import { usePathname } from "next/navigation";
import { registerServiceWorker } from "@/lib/registerSW";
import { NetworkStatus } from "../ui/NetworkStatus";
import { DebugWrapper } from "@/components/debug/DebugWrapper";
import { DebugProvider } from "@/contexts/DebugContext";
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
// Routes qui ne nécessitent pas d'authentification
@@ -68,6 +69,7 @@ export default function ClientLayout({ children, initialLibraries = [], initialF
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<DebugProvider>
<div className="relative min-h-screen h-full">
{!isPublicRoute && <Header onToggleSidebar={handleToggleSidebar} />}
{!isPublicRoute && (
@@ -84,6 +86,7 @@ export default function ClientLayout({ children, initialLibraries = [], initialF
<NetworkStatus />
<DebugWrapper />
</div>
</DebugProvider>
</ThemeProvider>
);
}

View File

@@ -1,6 +1,7 @@
import { useCallback, useRef } from "react";
import type { PageCache } from "../types";
import type { KomgaBook } from "@/types/komga";
import { usePreferences } from "@/contexts/PreferencesContext";
interface UsePageCacheProps {
book: KomgaBook;
@@ -9,6 +10,7 @@ interface UsePageCacheProps {
export const usePageCache = ({ book, pages }: UsePageCacheProps) => {
const pageCache = useRef<PageCache>({});
const { preferences } = usePreferences();
const preloadPage = useCallback(
async (pageNumber: number) => {
@@ -32,9 +34,29 @@ export const usePageCache = ({ book, pages }: UsePageCacheProps) => {
};
try {
const startTime = performance.now();
const response = await fetch(`/api/komga/books/${book.id}/pages/${pageNumber}`);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const endTime = performance.now();
// Logger la requête côté client seulement si le mode debug est activé et ce n'est pas une requête de debug
if (!url.includes('/api/debug') && preferences.debug) {
try {
await fetch("/api/debug", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: `/api/komga/books/${book.id}/pages/${pageNumber}`,
startTime,
endTime,
fromCache: false,
}),
});
} catch {
// Ignorer les erreurs de logging
}
}
pageCache.current[pageNumber] = {
blob,
@@ -49,12 +71,30 @@ export const usePageCache = ({ book, pages }: UsePageCacheProps) => {
resolveLoading!();
}
},
[book.id, pages.length]
[book.id, pages.length, preferences.debug]
);
const getPageUrl = useCallback(
async (pageNumber: number) => {
if (pageCache.current[pageNumber]?.url) {
// Logger l'utilisation du cache côté client seulement si le mode debug est activé et ce n'est pas une requête de debug
const cacheUrl = `[CLIENT-CACHE] /api/komga/books/${book.id}/pages/${pageNumber}`;
if (!cacheUrl.includes('/api/debug') && preferences.debug) {
try {
await fetch("/api/debug", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: cacheUrl,
startTime: performance.now(),
endTime: performance.now(),
fromCache: true,
}),
});
} catch {
// Ignorer les erreurs de logging
}
}
return pageCache.current[pageNumber].url;
}
@@ -69,7 +109,7 @@ export const usePageCache = ({ book, pages }: UsePageCacheProps) => {
`/api/komga/images/books/${book.id}/pages/${pageNumber}`
);
},
[book.id, preloadPage]
[book.id, preloadPage, preferences.debug]
);
const cleanCache = useCallback(