feat: add caching debug logs and configurable max concurrent requests for Komga API to enhance performance monitoring
This commit is contained in:
@@ -1,105 +0,0 @@
|
||||
import type { NextRequest} from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import type { RequestTiming } from "@/lib/services/debug.service";
|
||||
import { DebugService } from "@/lib/services/debug.service";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import { getErrorMessage } from "@/utils/errors";
|
||||
import { AppError } from "@/utils/errors";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const logs: RequestTiming[] = await DebugService.getRequestLogs();
|
||||
return NextResponse.json(logs);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération des logs:", error);
|
||||
if (error instanceof AppError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: error.code,
|
||||
name: "Debug fetch error",
|
||||
message: getErrorMessage(error.code),
|
||||
} as AppError,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: ERROR_CODES.DEBUG.FETCH_ERROR,
|
||||
name: "Debug fetch error",
|
||||
message: getErrorMessage(ERROR_CODES.DEBUG.FETCH_ERROR),
|
||||
} as AppError,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const timing: RequestTiming = await request.json();
|
||||
await DebugService.logRequest(timing);
|
||||
return NextResponse.json({
|
||||
message: "✅ Log enregistré avec succès",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de l'enregistrement du log:", error);
|
||||
if (error instanceof AppError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: error.code,
|
||||
name: "Debug save error",
|
||||
message: getErrorMessage(error.code),
|
||||
} as AppError,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: ERROR_CODES.DEBUG.SAVE_ERROR,
|
||||
name: "Debug save error",
|
||||
message: getErrorMessage(ERROR_CODES.DEBUG.SAVE_ERROR),
|
||||
} as AppError,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE() {
|
||||
try {
|
||||
await DebugService.clearLogs();
|
||||
return NextResponse.json({
|
||||
message: "🧹 Logs supprimés avec succès",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la suppression des logs:", error);
|
||||
if (error instanceof AppError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: error.code,
|
||||
name: "Debug clear error",
|
||||
message: getErrorMessage(error.code),
|
||||
} as AppError,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: ERROR_CODES.DEBUG.CLEAR_ERROR,
|
||||
name: "Debug clear error",
|
||||
message: getErrorMessage(ERROR_CODES.DEBUG.CLEAR_ERROR),
|
||||
} as AppError,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,7 @@ export const revalidate = 60;
|
||||
export async function GET() {
|
||||
try {
|
||||
const data = await HomeService.getHomeData();
|
||||
return NextResponse.json(data, {
|
||||
headers: {
|
||||
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=120'
|
||||
}
|
||||
});
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error("API Home - Erreur:", error);
|
||||
if (error instanceof AppError) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { BookService } from "@/lib/services/book.service";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import { getErrorMessage } from "@/utils/errors";
|
||||
import { findHttpStatus } from "@/utils/image-errors";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -18,6 +19,26 @@ export async function GET(
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération de la page du livre:", error);
|
||||
|
||||
// Chercher un status HTTP 404 dans la chaîne d'erreurs
|
||||
const httpStatus = findHttpStatus(error);
|
||||
|
||||
if (httpStatus === 404) {
|
||||
const { bookId, pageNumber } = await params;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`📷 Page ${pageNumber} not found for book: ${bookId}`);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: ERROR_CODES.IMAGE.FETCH_ERROR,
|
||||
name: "Image not found",
|
||||
message: "Image not found",
|
||||
},
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof AppError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ import { BookService } from "@/lib/services/book.service";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import { getErrorMessage } from "@/utils/errors";
|
||||
import { findHttpStatus } from "@/utils/image-errors";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -32,6 +33,27 @@ export async function GET(
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération de la miniature de la page:", error);
|
||||
|
||||
// Chercher un status HTTP 404 dans la chaîne d'erreurs
|
||||
const httpStatus = findHttpStatus(error);
|
||||
|
||||
if (httpStatus === 404) {
|
||||
const { bookId, pageNumber: pageNumberParam } = await params;
|
||||
const pageNumber: number = parseInt(pageNumberParam);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`📷 Page ${pageNumber} thumbnail not found for book: ${bookId}`);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: ERROR_CODES.IMAGE.FETCH_ERROR,
|
||||
name: "Image not found",
|
||||
message: "Image not found",
|
||||
},
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof AppError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ import { BookService } from "@/lib/services/book.service";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import { getErrorMessage } from "@/utils/errors";
|
||||
import { findHttpStatus } from "@/utils/image-errors";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
@@ -16,6 +17,26 @@ export async function GET(
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération de la miniature du livre:", error);
|
||||
|
||||
// Chercher un status HTTP 404 dans la chaîne d'erreurs
|
||||
const httpStatus = findHttpStatus(error);
|
||||
|
||||
if (httpStatus === 404) {
|
||||
const bookId: string = (await params).bookId;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`📷 Thumbnail not found for book: ${bookId}`);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: ERROR_CODES.IMAGE.FETCH_ERROR,
|
||||
name: "Image not found",
|
||||
message: "Image not found",
|
||||
},
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof AppError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SeriesService } from "@/lib/services/series.service";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import { getErrorMessage } from "@/utils/errors";
|
||||
import { findHttpStatus } from "@/utils/image-errors";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -18,6 +19,26 @@ export async function GET(
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération de la couverture de la série:", error);
|
||||
|
||||
// Chercher un status HTTP 404 dans la chaîne d'erreurs
|
||||
const httpStatus = findHttpStatus(error);
|
||||
|
||||
if (httpStatus === 404) {
|
||||
const seriesId: string = (await params).seriesId;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`📷 First page image not found for series: ${seriesId}`);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: ERROR_CODES.IMAGE.FETCH_ERROR,
|
||||
name: "Image not found",
|
||||
message: "Image not found",
|
||||
},
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof AppError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SeriesService } from "@/lib/services/series.service";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import { getErrorMessage } from "@/utils/errors";
|
||||
import { findHttpStatus } from "@/utils/image-errors";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
@@ -15,6 +16,26 @@ export async function GET(
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération de la miniature de la série:", error);
|
||||
|
||||
// Chercher un status HTTP 404 dans la chaîne d'erreurs
|
||||
const httpStatus = findHttpStatus(error);
|
||||
|
||||
if (httpStatus === 404) {
|
||||
const seriesId: string = (await params).seriesId;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`📷 Image not found for series: ${seriesId}`);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: ERROR_CODES.IMAGE.FETCH_ERROR,
|
||||
name: "Image not found",
|
||||
message: "Image not found",
|
||||
},
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof AppError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Suspense } from "react";
|
||||
import { ClientBookPage } from "@/components/reader/ClientBookPage";
|
||||
import { BookSkeleton } from "@/components/skeletons/BookSkeleton";
|
||||
import { withPageTiming } from "@/lib/hoc/withPageTiming";
|
||||
|
||||
async function BookPage({ params }: { params: { bookId: string } }) {
|
||||
export default async function BookPage({ params }: { params: Promise<{ bookId: string }> }) {
|
||||
const { bookId } = await params;
|
||||
|
||||
return (
|
||||
@@ -12,5 +11,3 @@ async function BookPage({ params }: { params: { bookId: string } }) {
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
export default withPageTiming("BookPage", BookPage);
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { DownloadManager } from "@/components/downloads/DownloadManager";
|
||||
import { withPageTiming } from "@/lib/hoc/withPageTiming";
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
function DownloadsPage() {
|
||||
export default function DownloadsPage() {
|
||||
return (
|
||||
<>
|
||||
<DownloadManager />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withPageTiming("DownloadsPage", DownloadsPage);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { LoginContent } from "./LoginContent";
|
||||
import { withPageTiming } from "@/lib/hoc/withPageTiming";
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{
|
||||
@@ -13,7 +12,6 @@ export const metadata: Metadata = {
|
||||
description: "Connectez-vous à votre compte StripStream",
|
||||
};
|
||||
|
||||
async function LoginPage({ searchParams }: PageProps) {
|
||||
export default async function LoginPage({ searchParams }: PageProps) {
|
||||
return <LoginContent searchParams={await searchParams} />;
|
||||
}
|
||||
export default withPageTiming("LoginPage", LoginPage);
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
X,
|
||||
Database,
|
||||
Minimize2,
|
||||
Maximize2,
|
||||
Clock,
|
||||
CircleDot,
|
||||
Layout,
|
||||
RefreshCw,
|
||||
Globe,
|
||||
Filter,
|
||||
Calendar,
|
||||
} from "lucide-react";
|
||||
import type { RequestTiming } from "@/types/debug";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDebug } from "@/contexts/DebugContext";
|
||||
|
||||
function formatTime(timestamp: string) {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString("fr-FR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
function formatDuration(duration: number) {
|
||||
return Math.round(duration);
|
||||
}
|
||||
|
||||
type FilterType = "all" | "current-page" | "api" | "cache" | "mongodb" | "page-render";
|
||||
|
||||
export function DebugInfo() {
|
||||
const { logs, setLogs, clearLogs, isRefreshing, setIsRefreshing } = useDebug();
|
||||
const [isMinimized, setIsMinimized] = useState(false);
|
||||
const [filter, setFilter] = useState<FilterType>("all");
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
setIsRefreshing(true);
|
||||
const response = await fetch("/api/debug");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setLogs(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération des logs:", error);
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 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
|
||||
className={`fixed bottom-4 right-4 bg-zinc-900/90 backdrop-blur-md border border-zinc-700 rounded-lg shadow-lg p-4 text-zinc-100 z-50 ${
|
||||
isMinimized ? "w-auto" : "w-[800px] max-h-[50vh] overflow-auto"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4 sticky top-0 bg-zinc-900/90 backdrop-blur-md pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="font-bold text-lg">{t("debug.title")}</h2>
|
||||
{!isMinimized && (
|
||||
<span className="text-xs text-zinc-400">
|
||||
{sortedLogs.length} {t("debug.entries", { count: sortedLogs.length })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{!isMinimized && (
|
||||
<button
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className={`hover:bg-zinc-700/80 hover:backdrop-blur-md rounded-full p-1.5 ${showFilters ? "bg-zinc-700/80 backdrop-blur-md" : ""}`}
|
||||
aria-label="Filtres"
|
||||
>
|
||||
<Filter className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={fetchLogs}
|
||||
className="hover:bg-zinc-700/80 hover:backdrop-blur-md rounded-full p-1.5"
|
||||
aria-label={t("debug.actions.refresh")}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw className={`h-5 w-5 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsMinimized(!isMinimized)}
|
||||
className="hover:bg-zinc-700/80 hover:backdrop-blur-md rounded-full p-1.5"
|
||||
aria-label={t(isMinimized ? "debug.actions.maximize" : "debug.actions.minimize")}
|
||||
>
|
||||
{isMinimized ? <Maximize2 className="h-5 w-5" /> : <Minimize2 className="h-5 w-5" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={clearLogs}
|
||||
className="hover:bg-zinc-700/80 hover:backdrop-blur-md rounded-full p-1.5"
|
||||
aria-label={t("debug.actions.clear")}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isMinimized && showFilters && (
|
||||
<div className="mb-4 p-3 bg-zinc-800/80 backdrop-blur-md 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) => {
|
||||
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" })}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<Database className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
{log.mongoAccess && (
|
||||
<div
|
||||
title={t("debug.tooltips.mongodb", {
|
||||
operation: log.mongoAccess.operation,
|
||||
})}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<CircleDot className="h-4 w-4 text-blue-400" />
|
||||
</div>
|
||||
)}
|
||||
{log.pageRender && (
|
||||
<div
|
||||
title={t("debug.tooltips.pageRender", { page: log.pageRender.page })}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<Layout className="h-4 w-4 text-purple-400" />
|
||||
</div>
|
||||
)}
|
||||
{!log.fromCache && !log.mongoAccess && !log.pageRender && (
|
||||
<div title={t("debug.tooltips.apiCall")} className="flex-shrink-0">
|
||||
<Globe className="h-4 w-4 text-rose-400" />
|
||||
</div>
|
||||
)}
|
||||
<span className="font-medium truncate" title={log.url}>
|
||||
{log.url}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 flex-shrink-0">
|
||||
<div
|
||||
className="flex items-center gap-1 text-zinc-400"
|
||||
title={t("debug.tooltips.logTime")}
|
||||
>
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{formatTime(log.timestamp)}</span>
|
||||
</div>
|
||||
<span
|
||||
className={`${
|
||||
log.pageRender
|
||||
? "text-purple-400"
|
||||
: log.mongoAccess
|
||||
? "text-blue-400"
|
||||
: log.fromCache
|
||||
? "text-emerald-400"
|
||||
: "text-rose-400"
|
||||
}`}
|
||||
>
|
||||
{formatDuration(log.duration)}ms
|
||||
</span>
|
||||
{log.mongoAccess && (
|
||||
<span className="text-blue-400" title={t("debug.tooltips.mongoAccess")}>
|
||||
+{formatDuration(log.mongoAccess.duration)}ms
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-1.5 bg-zinc-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${
|
||||
log.pageRender
|
||||
? "bg-purple-500"
|
||||
: log.fromCache
|
||||
? "bg-emerald-500"
|
||||
: "bg-rose-500"
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.min((log.duration / 1000) * 100, 100)}%`,
|
||||
}}
|
||||
/>
|
||||
{log.mongoAccess && (
|
||||
<div
|
||||
className="h-full bg-blue-500"
|
||||
style={{
|
||||
width: `${Math.min((log.mongoAccess.duration / 1000) * 100, 100)}%`,
|
||||
marginTop: "-6px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { usePreferences } from "@/contexts/PreferencesContext";
|
||||
import { DebugInfo } from "./DebugInfo";
|
||||
|
||||
export function DebugWrapper() {
|
||||
const { preferences } = usePreferences();
|
||||
if (!preferences.debug) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <DebugInfo />;
|
||||
}
|
||||
@@ -9,8 +9,6 @@ import { Toaster } from "@/components/ui/toaster";
|
||||
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 { usePreferences } from "@/contexts/PreferencesContext";
|
||||
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
||||
|
||||
@@ -94,25 +92,22 @@ export default function ClientLayout({ children, initialLibraries = [], initialF
|
||||
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<DebugProvider>
|
||||
<div className="relative min-h-screen bg-background" style={backgroundStyle}>
|
||||
{!isPublicRoute && <Header onToggleSidebar={handleToggleSidebar} />}
|
||||
{!isPublicRoute && (
|
||||
<Sidebar
|
||||
isOpen={isSidebarOpen}
|
||||
onClose={handleCloseSidebar}
|
||||
initialLibraries={initialLibraries}
|
||||
initialFavorites={initialFavorites}
|
||||
userIsAdmin={userIsAdmin}
|
||||
/>
|
||||
)}
|
||||
<main className={`${!isPublicRoute ? "container pt-safe" : ""}`}>{children}</main>
|
||||
<InstallPWA />
|
||||
<Toaster />
|
||||
<NetworkStatus />
|
||||
<DebugWrapper />
|
||||
</div>
|
||||
</DebugProvider>
|
||||
<div className="relative min-h-screen bg-background" style={backgroundStyle}>
|
||||
{!isPublicRoute && <Header onToggleSidebar={handleToggleSidebar} />}
|
||||
{!isPublicRoute && (
|
||||
<Sidebar
|
||||
isOpen={isSidebarOpen}
|
||||
onClose={handleCloseSidebar}
|
||||
initialLibraries={initialLibraries}
|
||||
initialFavorites={initialFavorites}
|
||||
userIsAdmin={userIsAdmin}
|
||||
/>
|
||||
)}
|
||||
<main className={`${!isPublicRoute ? "container pt-safe" : ""}`}>{children}</main>
|
||||
<InstallPWA />
|
||||
<Toaster />
|
||||
<NetworkStatus />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,34 +75,6 @@ export function DisplaySettings() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="debug-mode">{t("settings.display.debugMode.label")}</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("settings.display.debugMode.description")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="debug-mode"
|
||||
checked={preferences.debug}
|
||||
onCheckedChange={async (checked) => {
|
||||
try {
|
||||
await updatePreferences({ debug: checked });
|
||||
toast({
|
||||
title: t("settings.title"),
|
||||
description: t("settings.komga.messages.configSaved"),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Erreur détaillée:", error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("settings.error.title"),
|
||||
description: t("settings.error.message"),
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -87,11 +87,6 @@ export const ERROR_CODES = {
|
||||
INVALID_TOKEN: "MIDDLEWARE_INVALID_TOKEN",
|
||||
INVALID_SESSION: "MIDDLEWARE_INVALID_SESSION",
|
||||
},
|
||||
DEBUG: {
|
||||
FETCH_ERROR: "DEBUG_FETCH_ERROR",
|
||||
SAVE_ERROR: "DEBUG_SAVE_ERROR",
|
||||
CLEAR_ERROR: "DEBUG_CLEAR_ERROR",
|
||||
},
|
||||
CLIENT: {
|
||||
FETCH_ERROR: "CLIENT_FETCH_ERROR",
|
||||
NETWORK_ERROR: "CLIENT_NETWORK_ERROR",
|
||||
|
||||
@@ -85,11 +85,6 @@ export const ERROR_MESSAGES: Record<string, string> = {
|
||||
[ERROR_CODES.CONFIG.TTL_SAVE_ERROR]: "⏱️ Error saving TTL configuration",
|
||||
[ERROR_CODES.CONFIG.TTL_FETCH_ERROR]: "⏱️ Error fetching TTL configuration",
|
||||
|
||||
// Debug
|
||||
[ERROR_CODES.DEBUG.FETCH_ERROR]: "🔍 Error fetching logs",
|
||||
[ERROR_CODES.DEBUG.SAVE_ERROR]: "💾 Error saving log",
|
||||
[ERROR_CODES.DEBUG.CLEAR_ERROR]: "🧹 Error clearing logs",
|
||||
|
||||
// Client
|
||||
[ERROR_CODES.CLIENT.FETCH_ERROR]: "🌐 Error during request",
|
||||
[ERROR_CODES.CLIENT.NETWORK_ERROR]: "📡 Network connection error",
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from "react";
|
||||
import type { RequestTiming } from "@/types/debug";
|
||||
import { usePreferences } from "./PreferencesContext";
|
||||
|
||||
interface DebugContextType {
|
||||
logs: RequestTiming[];
|
||||
setLogs: (logs: RequestTiming[]) => void;
|
||||
addLog: (log: RequestTiming) => void;
|
||||
clearLogs: () => void;
|
||||
isRefreshing: boolean;
|
||||
setIsRefreshing: (refreshing: boolean) => void;
|
||||
}
|
||||
|
||||
const DebugContext = createContext<DebugContextType | undefined>(undefined);
|
||||
|
||||
interface DebugProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function DebugProvider({ children }: DebugProviderProps) {
|
||||
const [logs, setLogs] = useState<RequestTiming[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const { preferences } = usePreferences();
|
||||
|
||||
const addLog = (log: RequestTiming) => {
|
||||
setLogs(prevLogs => {
|
||||
// Éviter les doublons basés sur l'URL et le timestamp
|
||||
const exists = prevLogs.some(existingLog =>
|
||||
existingLog.url === log.url && existingLog.timestamp === log.timestamp
|
||||
);
|
||||
if (exists) return prevLogs;
|
||||
|
||||
return [...prevLogs, log].sort((a, b) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const clearLogs = async () => {
|
||||
try {
|
||||
// Vider le fichier côté serveur
|
||||
await fetch("/api/debug", { method: "DELETE" });
|
||||
// Vider le state côté client
|
||||
setLogs([]);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la suppression des logs:", error);
|
||||
// Même en cas d'erreur, vider le state côté client
|
||||
setLogs([]);
|
||||
}
|
||||
};
|
||||
|
||||
// Charger les logs au montage du provider et les rafraîchir périodiquement
|
||||
useEffect(() => {
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
// Ne pas faire de requête si le debug n'est pas activé
|
||||
if (!preferences.debug) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRefreshing(true);
|
||||
const debugResponse = await fetch("/api/debug");
|
||||
if (debugResponse.ok) {
|
||||
const serverLogs = await debugResponse.json();
|
||||
setLogs(serverLogs);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération des logs:", error);
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchLogs();
|
||||
|
||||
// Rafraîchir toutes les 10 secondes (moins fréquent pour éviter les conflits)
|
||||
const interval = setInterval(fetchLogs, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [preferences.debug]);
|
||||
|
||||
return (
|
||||
<DebugContext.Provider
|
||||
value={{
|
||||
logs,
|
||||
setLogs,
|
||||
addLog,
|
||||
clearLogs,
|
||||
isRefreshing,
|
||||
setIsRefreshing
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DebugContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useDebug() {
|
||||
const context = useContext(DebugContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useDebug must be used within a DebugProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -77,10 +77,6 @@
|
||||
"unreadFilter": {
|
||||
"label": "Default Unread Filter",
|
||||
"description": "Show only unread series by default"
|
||||
},
|
||||
"debugMode": {
|
||||
"label": "Debug mode",
|
||||
"description": "Show debug information in the interface"
|
||||
}
|
||||
},
|
||||
"background": {
|
||||
@@ -388,10 +384,6 @@
|
||||
"MIDDLEWARE_INVALID_TOKEN": "Invalid authentication token",
|
||||
"MIDDLEWARE_INVALID_SESSION": "Invalid session",
|
||||
|
||||
"DEBUG_FETCH_ERROR": "Error fetching debug logs",
|
||||
"DEBUG_SAVE_ERROR": "Error saving debug logs",
|
||||
"DEBUG_CLEAR_ERROR": "Error clearing debug logs",
|
||||
|
||||
"CLIENT_FETCH_ERROR": "Error fetching data",
|
||||
"CLIENT_NETWORK_ERROR": "Network error",
|
||||
"CLIENT_REQUEST_FAILED": "Request failed",
|
||||
@@ -434,25 +426,5 @@
|
||||
},
|
||||
"common": {
|
||||
"retry": "Retry"
|
||||
},
|
||||
"debug": {
|
||||
"title": "DEBUG",
|
||||
"entries": "entry",
|
||||
"entries_plural": "entries",
|
||||
"noRequests": "No recorded requests",
|
||||
"actions": {
|
||||
"refresh": "Refresh logs",
|
||||
"maximize": "Maximize",
|
||||
"minimize": "Minimize",
|
||||
"clear": "Clear logs"
|
||||
},
|
||||
"tooltips": {
|
||||
"cache": "Cache: {{type}}",
|
||||
"mongodb": "MongoDB: {{operation}}",
|
||||
"pageRender": "Page Render: {{page}}",
|
||||
"apiCall": "API Call",
|
||||
"logTime": "Log time",
|
||||
"mongoAccess": "MongoDB access time"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,10 +77,6 @@
|
||||
"unreadFilter": {
|
||||
"label": "Filtre \"À lire\" par défaut",
|
||||
"description": "Afficher uniquement les séries non lues par défaut"
|
||||
},
|
||||
"debugMode": {
|
||||
"label": "Mode debug",
|
||||
"description": "Afficher les informations de debug dans l'interface"
|
||||
}
|
||||
},
|
||||
"background": {
|
||||
@@ -386,10 +382,6 @@
|
||||
"MIDDLEWARE_INVALID_TOKEN": "Jeton d'authentification invalide",
|
||||
"MIDDLEWARE_INVALID_SESSION": "Session invalide",
|
||||
|
||||
"DEBUG_FETCH_ERROR": "Erreur lors de la récupération des logs de debug",
|
||||
"DEBUG_SAVE_ERROR": "Erreur lors de la sauvegarde des logs de debug",
|
||||
"DEBUG_CLEAR_ERROR": "Erreur lors de la suppression des logs de debug",
|
||||
|
||||
"CLIENT_FETCH_ERROR": "Erreur lors de la récupération des données",
|
||||
"CLIENT_NETWORK_ERROR": "Erreur réseau",
|
||||
"CLIENT_REQUEST_FAILED": "La requête a échoué",
|
||||
@@ -436,25 +428,5 @@
|
||||
},
|
||||
"common": {
|
||||
"retry": "Réessayer"
|
||||
},
|
||||
"debug": {
|
||||
"title": "DEBUG",
|
||||
"entries": "entrée",
|
||||
"entries_plural": "entrées",
|
||||
"noRequests": "Aucune requête enregistrée",
|
||||
"actions": {
|
||||
"refresh": "Rafraîchir les logs",
|
||||
"maximize": "Agrandir",
|
||||
"minimize": "Minimiser",
|
||||
"clear": "Effacer les logs"
|
||||
},
|
||||
"tooltips": {
|
||||
"cache": "Cache: {{type}}",
|
||||
"mongodb": "MongoDB: {{operation}}",
|
||||
"pageRender": "Page Render: {{page}}",
|
||||
"apiCall": "API Call",
|
||||
"logTime": "Heure du log",
|
||||
"mongoAccess": "Temps d'accès MongoDB"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ReactElement } from "react";
|
||||
import { DebugService } from "@/lib/services/debug.service";
|
||||
|
||||
type PageComponent = (props: any) => Promise<ReactElement> | ReactElement;
|
||||
|
||||
export function withPageTiming(pageName: string, Component: PageComponent) {
|
||||
return async function PageWithTiming(props: any) {
|
||||
const start = performance.now();
|
||||
const result = await Promise.resolve(Component(props));
|
||||
const duration = performance.now() - start;
|
||||
|
||||
// Ensure params is awaited before using it
|
||||
const params = props.params ? await Promise.resolve(props.params) : {};
|
||||
|
||||
// Only log if debug is enabled and user is authenticated
|
||||
try {
|
||||
await DebugService.logPageRender(pageName + JSON.stringify(params), duration);
|
||||
} catch {
|
||||
// Silently fail if user is not authenticated or debug is disabled
|
||||
// This prevents errors on public pages like /login
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { ERROR_CODES } from "../../constants/errorCodes";
|
||||
import { AppError } from "../../utils/errors";
|
||||
import type { KomgaConfig } from "@/types/komga";
|
||||
import type { ServerCacheService } from "./server-cache.service";
|
||||
import { DebugService } from "./debug.service";
|
||||
import { RequestMonitorService } from "./request-monitor.service";
|
||||
import { RequestQueueService } from "./request-queue.service";
|
||||
|
||||
@@ -109,8 +108,6 @@ export abstract class BaseApiService {
|
||||
}
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Timeout de 60 secondes au lieu de 10 par défaut
|
||||
const timeoutMs = 60000;
|
||||
const controller = new AbortController();
|
||||
@@ -149,19 +146,27 @@ export abstract class BaseApiService {
|
||||
family: 4,
|
||||
});
|
||||
}
|
||||
|
||||
// Retry automatique sur timeout de connexion (cold start)
|
||||
if (fetchError?.cause?.code === 'UND_ERR_CONNECT_TIMEOUT') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`⏱️ Connection timeout for ${url}. Retrying once (cold start)...`);
|
||||
|
||||
return await fetch(url, {
|
||||
headers,
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
// @ts-ignore - undici-specific options
|
||||
connectTimeout: timeoutMs,
|
||||
bodyTimeout: timeoutMs,
|
||||
headersTimeout: timeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
throw fetchError;
|
||||
}
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
const endTime = performance.now();
|
||||
|
||||
// Logger la requête côté serveur
|
||||
await DebugService.logRequest({
|
||||
url: url,
|
||||
startTime,
|
||||
endTime,
|
||||
fromCache: false, // Côté serveur, on ne peut pas détecter le cache navigateur
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new AppError(ERROR_CODES.KOMGA.HTTP_ERROR, {
|
||||
@@ -172,16 +177,6 @@ export abstract class BaseApiService {
|
||||
|
||||
return options.isImage ? (response as T) : response.json();
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
|
||||
// Logger l'erreur côté serveur
|
||||
await DebugService.logRequest({
|
||||
url: url,
|
||||
startTime,
|
||||
endTime,
|
||||
fromCache: false,
|
||||
});
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
@@ -99,7 +99,7 @@ export class BookService extends BaseApiService {
|
||||
const arrayBuffer = response.buffer.buffer.slice(
|
||||
response.buffer.byteOffset,
|
||||
response.buffer.byteOffset + response.buffer.byteLength
|
||||
);
|
||||
) as ArrayBuffer;
|
||||
|
||||
return new Response(arrayBuffer, {
|
||||
headers: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { DebugService } from "./debug.service";
|
||||
import { getCurrentUser } from "../auth-utils";
|
||||
import { ERROR_CODES } from "../../constants/errorCodes";
|
||||
import { AppError } from "../../utils/errors";
|
||||
@@ -50,12 +49,10 @@ export class ConfigDBService {
|
||||
try {
|
||||
const user: User | null = await this.getCurrentUser();
|
||||
|
||||
return DebugService.measureMongoOperation("getConfig", async () => {
|
||||
const config = await prisma.komgaConfig.findUnique({
|
||||
where: { userId: user.id },
|
||||
});
|
||||
return config as KomgaConfig | null;
|
||||
const config = await prisma.komgaConfig.findUnique({
|
||||
where: { userId: user.id },
|
||||
});
|
||||
return config as KomgaConfig | null;
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
@@ -68,12 +65,10 @@ export class ConfigDBService {
|
||||
try {
|
||||
const user: User | null = await this.getCurrentUser();
|
||||
|
||||
return DebugService.measureMongoOperation("getTTLConfig", async () => {
|
||||
const config = await prisma.tTLConfig.findUnique({
|
||||
where: { userId: user.id },
|
||||
});
|
||||
return config as TTLConfig | null;
|
||||
const config = await prisma.tTLConfig.findUnique({
|
||||
where: { userId: user.id },
|
||||
});
|
||||
return config as TTLConfig | null;
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
@@ -86,30 +81,28 @@ export class ConfigDBService {
|
||||
try {
|
||||
const user: User | null = await this.getCurrentUser();
|
||||
|
||||
return DebugService.measureMongoOperation("saveTTLConfig", async () => {
|
||||
const config = await prisma.tTLConfig.upsert({
|
||||
where: { userId: user.id },
|
||||
update: {
|
||||
defaultTTL: data.defaultTTL,
|
||||
homeTTL: data.homeTTL,
|
||||
librariesTTL: data.librariesTTL,
|
||||
seriesTTL: data.seriesTTL,
|
||||
booksTTL: data.booksTTL,
|
||||
imagesTTL: data.imagesTTL,
|
||||
},
|
||||
create: {
|
||||
userId: user.id,
|
||||
defaultTTL: data.defaultTTL,
|
||||
homeTTL: data.homeTTL,
|
||||
librariesTTL: data.librariesTTL,
|
||||
seriesTTL: data.seriesTTL,
|
||||
booksTTL: data.booksTTL,
|
||||
imagesTTL: data.imagesTTL,
|
||||
},
|
||||
});
|
||||
|
||||
return config as TTLConfig;
|
||||
const config = await prisma.tTLConfig.upsert({
|
||||
where: { userId: user.id },
|
||||
update: {
|
||||
defaultTTL: data.defaultTTL,
|
||||
homeTTL: data.homeTTL,
|
||||
librariesTTL: data.librariesTTL,
|
||||
seriesTTL: data.seriesTTL,
|
||||
booksTTL: data.booksTTL,
|
||||
imagesTTL: data.imagesTTL,
|
||||
},
|
||||
create: {
|
||||
userId: user.id,
|
||||
defaultTTL: data.defaultTTL,
|
||||
homeTTL: data.homeTTL,
|
||||
librariesTTL: data.librariesTTL,
|
||||
seriesTTL: data.seriesTTL,
|
||||
booksTTL: data.booksTTL,
|
||||
imagesTTL: data.imagesTTL,
|
||||
},
|
||||
});
|
||||
|
||||
return config as TTLConfig;
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
|
||||
@@ -1,326 +0,0 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import type { RequestTiming } from "@/types/debug";
|
||||
import { PreferencesService } from "./preferences.service";
|
||||
import { getCurrentUser } from "../auth-utils";
|
||||
import { ERROR_CODES } from "../../constants/errorCodes";
|
||||
import { AppError } from "../../utils/errors";
|
||||
|
||||
export type { RequestTiming };
|
||||
|
||||
export class DebugService {
|
||||
private static writeQueues = new Map<string, Promise<void>>();
|
||||
|
||||
private static async getCurrentUserId(): Promise<string> {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
throw new AppError(ERROR_CODES.AUTH.UNAUTHENTICATED);
|
||||
}
|
||||
return user.id;
|
||||
}
|
||||
|
||||
private static getLogFilePath(userId: string): string {
|
||||
return path.join(process.cwd(), "debug-logs", `${userId}.json`);
|
||||
}
|
||||
|
||||
private static async ensureDebugDir(): Promise<void> {
|
||||
const debugDir = path.join(process.cwd(), "debug-logs");
|
||||
try {
|
||||
await fs.access(debugDir);
|
||||
} catch {
|
||||
await fs.mkdir(debugDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
private static async isDebugEnabled(): Promise<boolean> {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
const preferences = await PreferencesService.getPreferences();
|
||||
return preferences.debug === true;
|
||||
}
|
||||
|
||||
private static async readLogs(filePath: string): Promise<RequestTiming[]> {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
// Essayer de lire un fichier de sauvegarde
|
||||
try {
|
||||
const backupPath = filePath + '.backup';
|
||||
const backupContent = await fs.readFile(backupPath, "utf-8");
|
||||
return JSON.parse(backupContent);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async writeLogs(filePath: string, logs: RequestTiming[]): Promise<void> {
|
||||
// Obtenir la queue existante ou créer une nouvelle
|
||||
const existingQueue = this.writeQueues.get(filePath);
|
||||
|
||||
// Créer une nouvelle promesse qui attend la queue précédente
|
||||
const newQueue = existingQueue
|
||||
? existingQueue.then(() => this.performAppend(filePath, logs))
|
||||
: this.performAppend(filePath, logs);
|
||||
|
||||
// Mettre à jour la queue
|
||||
this.writeQueues.set(filePath, newQueue);
|
||||
|
||||
try {
|
||||
await newQueue;
|
||||
} finally {
|
||||
// Nettoyer la queue si c'est la dernière opération
|
||||
if (this.writeQueues.get(filePath) === newQueue) {
|
||||
this.writeQueues.delete(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async performAppend(filePath: string, logs: RequestTiming[]): Promise<void> {
|
||||
try {
|
||||
// Lire le fichier existant
|
||||
const existingLogs = await this.readLogs(filePath);
|
||||
|
||||
// Fusionner avec les nouveaux logs
|
||||
const allLogs = [...existingLogs, ...logs];
|
||||
|
||||
// Garder seulement les 1000 derniers logs
|
||||
const trimmedLogs = allLogs.slice(-1000);
|
||||
|
||||
// Créer une sauvegarde avant d'écrire
|
||||
try {
|
||||
await fs.copyFile(filePath, filePath + '.backup');
|
||||
} catch {
|
||||
// Ignorer si le fichier n'existe pas encore
|
||||
}
|
||||
|
||||
// Écrire le fichier complet (c'est nécessaire pour maintenir l'ordre chronologique)
|
||||
await fs.writeFile(filePath, JSON.stringify(trimmedLogs, null, 2), { flag: 'w' });
|
||||
} catch (error) {
|
||||
console.error(`Erreur lors de l'écriture des logs pour ${filePath}:`, error);
|
||||
// Ne pas relancer l'erreur pour éviter de casser l'application
|
||||
}
|
||||
}
|
||||
|
||||
private static async appendLog(filePath: string, log: RequestTiming): Promise<void> {
|
||||
// Obtenir la queue existante ou créer une nouvelle
|
||||
const existingQueue = this.writeQueues.get(filePath);
|
||||
|
||||
// Créer une nouvelle promesse qui attend la queue précédente
|
||||
const newQueue = existingQueue
|
||||
? existingQueue.then(() => this.performSingleAppend(filePath, log))
|
||||
: this.performSingleAppend(filePath, log);
|
||||
|
||||
// Mettre à jour la queue
|
||||
this.writeQueues.set(filePath, newQueue);
|
||||
|
||||
try {
|
||||
await newQueue;
|
||||
} finally {
|
||||
// Nettoyer la queue si c'est la dernière opération
|
||||
if (this.writeQueues.get(filePath) === newQueue) {
|
||||
this.writeQueues.delete(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async performSingleAppend(filePath: string, log: RequestTiming): Promise<void> {
|
||||
try {
|
||||
// Lire le fichier existant
|
||||
const existingLogs = await this.readLogs(filePath);
|
||||
|
||||
// Vérifier les doublons avec des tolérances différentes selon le type
|
||||
const isPageRender = log.pageRender !== undefined;
|
||||
const timeTolerance = isPageRender ? 500 : 50; // 500ms pour les rendus, 50ms pour les requêtes
|
||||
|
||||
const exists = existingLogs.some(existingLog =>
|
||||
existingLog.url === log.url &&
|
||||
Math.abs(existingLog.duration - log.duration) < 10 && // Durée similaire (10ms de tolérance)
|
||||
Math.abs(new Date(existingLog.timestamp).getTime() - new Date(log.timestamp).getTime()) < timeTolerance
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
// Ajouter le nouveau log
|
||||
const allLogs = [...existingLogs, log];
|
||||
|
||||
// Garder seulement les 1000 derniers logs
|
||||
const trimmedLogs = allLogs.slice(-1000);
|
||||
|
||||
// Écrire le fichier complet avec gestion d'erreur
|
||||
await fs.writeFile(filePath, JSON.stringify(trimmedLogs, null, 2), { flag: 'w' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Erreur lors de l'écriture du log pour ${filePath}:`, error);
|
||||
// Ne pas relancer l'erreur pour éviter de casser l'application
|
||||
}
|
||||
}
|
||||
|
||||
private static createTiming(
|
||||
url: string,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
fromCache: boolean,
|
||||
additionalData?: Partial<RequestTiming>
|
||||
): RequestTiming {
|
||||
return {
|
||||
url,
|
||||
startTime,
|
||||
endTime,
|
||||
duration: endTime - startTime,
|
||||
timestamp: new Date().toISOString(),
|
||||
fromCache,
|
||||
...additionalData,
|
||||
};
|
||||
}
|
||||
|
||||
static async logRequest(timing: Omit<RequestTiming, "duration" | "timestamp">) {
|
||||
try {
|
||||
if (!(await this.isDebugEnabled())) return;
|
||||
|
||||
const userId = await this.getCurrentUserId();
|
||||
await this.ensureDebugDir();
|
||||
const filePath = this.getLogFilePath(userId);
|
||||
|
||||
const newTiming = this.createTiming(
|
||||
timing.url,
|
||||
timing.startTime,
|
||||
timing.endTime,
|
||||
timing.fromCache,
|
||||
{
|
||||
cacheType: timing.cacheType,
|
||||
mongoAccess: timing.mongoAccess,
|
||||
pageRender: timing.pageRender,
|
||||
}
|
||||
);
|
||||
|
||||
// Utiliser un système d'append atomique
|
||||
await this.appendLog(filePath, newTiming);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de l'enregistrement du log:", error);
|
||||
}
|
||||
}
|
||||
|
||||
static async getRequestLogs(): Promise<RequestTiming[]> {
|
||||
try {
|
||||
const userId = await this.getCurrentUserId();
|
||||
const filePath = this.getLogFilePath(userId);
|
||||
return await this.readLogs(filePath);
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) throw error;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async clearLogs(): Promise<void> {
|
||||
try {
|
||||
const userId = await this.getCurrentUserId();
|
||||
const filePath = this.getLogFilePath(userId);
|
||||
await this.clearFile(filePath);
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private static async clearFile(filePath: string): Promise<void> {
|
||||
try {
|
||||
// Obtenir la queue existante ou créer une nouvelle
|
||||
const existingQueue = this.writeQueues.get(filePath);
|
||||
|
||||
// Créer une nouvelle promesse qui attend la queue précédente
|
||||
const newQueue = existingQueue
|
||||
? existingQueue.then(() => this.performClear(filePath))
|
||||
: this.performClear(filePath);
|
||||
|
||||
// Mettre à jour la queue
|
||||
this.writeQueues.set(filePath, newQueue);
|
||||
|
||||
try {
|
||||
await newQueue;
|
||||
} finally {
|
||||
// Nettoyer la queue si c'est la dernière opération
|
||||
if (this.writeQueues.get(filePath) === newQueue) {
|
||||
this.writeQueues.delete(filePath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Erreur lors du vidage du fichier ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private static async performClear(filePath: string): Promise<void> {
|
||||
try {
|
||||
// Créer une sauvegarde avant de vider
|
||||
try {
|
||||
await fs.copyFile(filePath, filePath + '.backup');
|
||||
} catch {
|
||||
// Ignorer si le fichier n'existe pas encore
|
||||
}
|
||||
|
||||
// Écrire un tableau vide pour vider le fichier
|
||||
await fs.writeFile(filePath, JSON.stringify([], null, 2), { flag: 'w' });
|
||||
} catch (error) {
|
||||
console.error(`Erreur lors du vidage du fichier ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
static async logPageRender(page: string, duration: number) {
|
||||
try {
|
||||
if (!(await this.isDebugEnabled())) return;
|
||||
|
||||
const userId = await this.getCurrentUserId();
|
||||
await this.ensureDebugDir();
|
||||
const filePath = this.getLogFilePath(userId);
|
||||
|
||||
const now = performance.now();
|
||||
const newTiming = this.createTiming(`Page Render: ${page}`, now - duration, now, false, {
|
||||
pageRender: { page, duration },
|
||||
});
|
||||
|
||||
// Utiliser le même système d'append atomique
|
||||
await this.appendLog(filePath, newTiming);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de l'enregistrement du log de rendu:", error);
|
||||
}
|
||||
}
|
||||
|
||||
static async measureMongoOperation<T>(operation: string, func: () => Promise<T>): Promise<T> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
if (!(await this.isDebugEnabled())) {
|
||||
return func();
|
||||
}
|
||||
|
||||
const result = await func();
|
||||
const endTime = performance.now();
|
||||
|
||||
await this.logRequest({
|
||||
url: `MongoDB: ${operation}`,
|
||||
startTime,
|
||||
endTime,
|
||||
fromCache: false,
|
||||
mongoAccess: {
|
||||
operation,
|
||||
duration: endTime - startTime,
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
await this.logRequest({
|
||||
url: `MongoDB Error: ${operation}`,
|
||||
startTime,
|
||||
endTime,
|
||||
fromCache: false,
|
||||
mongoAccess: {
|
||||
operation,
|
||||
duration: endTime - startTime,
|
||||
},
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { DebugService } from "./debug.service";
|
||||
import { getCurrentUser } from "../auth-utils";
|
||||
import { ERROR_CODES } from "../../constants/errorCodes";
|
||||
import { AppError } from "../../utils/errors";
|
||||
@@ -30,15 +29,13 @@ export class FavoriteService {
|
||||
try {
|
||||
const user = await this.getCurrentUser();
|
||||
|
||||
return DebugService.measureMongoOperation("isFavorite", async () => {
|
||||
const favorite = await prisma.favorite.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
seriesId: seriesId,
|
||||
},
|
||||
});
|
||||
return !!favorite;
|
||||
const favorite = await prisma.favorite.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
seriesId: seriesId,
|
||||
},
|
||||
});
|
||||
return !!favorite;
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la vérification du favori:", error);
|
||||
return false;
|
||||
@@ -52,20 +49,18 @@ export class FavoriteService {
|
||||
try {
|
||||
const user = await this.getCurrentUser();
|
||||
|
||||
await DebugService.measureMongoOperation("addToFavorites", async () => {
|
||||
await prisma.favorite.upsert({
|
||||
where: {
|
||||
userId_seriesId: {
|
||||
userId: user.id,
|
||||
seriesId,
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
await prisma.favorite.upsert({
|
||||
where: {
|
||||
userId_seriesId: {
|
||||
userId: user.id,
|
||||
seriesId,
|
||||
},
|
||||
});
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
userId: user.id,
|
||||
seriesId,
|
||||
},
|
||||
});
|
||||
|
||||
this.dispatchFavoritesChanged();
|
||||
@@ -81,13 +76,11 @@ export class FavoriteService {
|
||||
try {
|
||||
const user = await this.getCurrentUser();
|
||||
|
||||
await DebugService.measureMongoOperation("removeFromFavorites", async () => {
|
||||
await prisma.favorite.deleteMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
seriesId,
|
||||
},
|
||||
});
|
||||
await prisma.favorite.deleteMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
seriesId,
|
||||
},
|
||||
});
|
||||
|
||||
this.dispatchFavoritesChanged();
|
||||
@@ -102,47 +95,41 @@ export class FavoriteService {
|
||||
static async getAllFavoriteIds(): Promise<string[]> {
|
||||
const user = await this.getCurrentUser();
|
||||
|
||||
return DebugService.measureMongoOperation("getAllFavoriteIds", async () => {
|
||||
const favorites = await prisma.favorite.findMany({
|
||||
where: { userId: user.id },
|
||||
select: { seriesId: true },
|
||||
});
|
||||
return favorites.map((favorite) => favorite.seriesId);
|
||||
const favorites = await prisma.favorite.findMany({
|
||||
where: { userId: user.id },
|
||||
select: { seriesId: true },
|
||||
});
|
||||
return favorites.map((favorite) => favorite.seriesId);
|
||||
}
|
||||
|
||||
static async addFavorite(seriesId: string) {
|
||||
const user = await this.getCurrentUser();
|
||||
|
||||
return DebugService.measureMongoOperation("addFavorite", async () => {
|
||||
const favorite = await prisma.favorite.upsert({
|
||||
where: {
|
||||
userId_seriesId: {
|
||||
userId: user.id,
|
||||
seriesId,
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
const favorite = await prisma.favorite.upsert({
|
||||
where: {
|
||||
userId_seriesId: {
|
||||
userId: user.id,
|
||||
seriesId,
|
||||
},
|
||||
});
|
||||
return favorite;
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
userId: user.id,
|
||||
seriesId,
|
||||
},
|
||||
});
|
||||
return favorite;
|
||||
}
|
||||
|
||||
static async removeFavorite(seriesId: string): Promise<boolean> {
|
||||
const user = await this.getCurrentUser();
|
||||
|
||||
return DebugService.measureMongoOperation("removeFavorite", async () => {
|
||||
const result = await prisma.favorite.deleteMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
seriesId,
|
||||
},
|
||||
});
|
||||
return result.count > 0;
|
||||
const result = await prisma.favorite.deleteMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
seriesId,
|
||||
},
|
||||
});
|
||||
return result.count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,9 +84,13 @@ export class LibraryService extends BaseApiService {
|
||||
try {
|
||||
// Récupérer toutes les séries depuis le cache
|
||||
const allSeries = await this.getAllLibrarySeries(libraryId);
|
||||
|
||||
// Filtrer les séries
|
||||
let filteredSeries = allSeries;
|
||||
|
||||
// Filtrer les séries supprimées (fichiers manquants sur le filesystem)
|
||||
filteredSeries = filteredSeries.filter((series) => !series.deleted);
|
||||
|
||||
if (unreadOnly) {
|
||||
filteredSeries = filteredSeries.filter(
|
||||
(series) => series.booksReadCount < series.booksCount
|
||||
@@ -96,7 +100,8 @@ export class LibraryService extends BaseApiService {
|
||||
if (search) {
|
||||
const searchLower = search.toLowerCase();
|
||||
filteredSeries = filteredSeries.filter((series) =>
|
||||
series.metadata.title.toLowerCase().includes(searchLower)
|
||||
series.metadata.title.toLowerCase().includes(searchLower) ||
|
||||
series.id.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,6 +113,7 @@ export class LibraryService extends BaseApiService {
|
||||
const totalPages = Math.ceil(totalElements / size);
|
||||
const startIndex = page * size;
|
||||
const endIndex = Math.min(startIndex + size, totalElements);
|
||||
|
||||
const paginatedSeries = filteredSeries.slice(startIndex, endIndex);
|
||||
|
||||
// Construire la réponse
|
||||
|
||||
@@ -31,7 +31,6 @@ export class PreferencesService {
|
||||
showThumbnails: preferences.showThumbnails,
|
||||
cacheMode: preferences.cacheMode as "memory" | "file",
|
||||
showOnlyUnread: preferences.showOnlyUnread,
|
||||
debug: preferences.debug,
|
||||
displayMode: preferences.displayMode as UserPreferences["displayMode"],
|
||||
background: preferences.background as unknown as BackgroundPreferences,
|
||||
};
|
||||
@@ -51,7 +50,6 @@ export class PreferencesService {
|
||||
if (preferences.showThumbnails !== undefined) updateData.showThumbnails = preferences.showThumbnails;
|
||||
if (preferences.cacheMode !== undefined) updateData.cacheMode = preferences.cacheMode;
|
||||
if (preferences.showOnlyUnread !== undefined) updateData.showOnlyUnread = preferences.showOnlyUnread;
|
||||
if (preferences.debug !== undefined) updateData.debug = preferences.debug;
|
||||
if (preferences.displayMode !== undefined) updateData.displayMode = preferences.displayMode;
|
||||
if (preferences.background !== undefined) updateData.background = preferences.background;
|
||||
|
||||
@@ -63,7 +61,6 @@ export class PreferencesService {
|
||||
showThumbnails: preferences.showThumbnails ?? defaultPreferences.showThumbnails,
|
||||
cacheMode: preferences.cacheMode ?? defaultPreferences.cacheMode,
|
||||
showOnlyUnread: preferences.showOnlyUnread ?? defaultPreferences.showOnlyUnread,
|
||||
debug: preferences.debug ?? defaultPreferences.debug,
|
||||
displayMode: preferences.displayMode ?? defaultPreferences.displayMode,
|
||||
background: (preferences.background ?? defaultPreferences.background) as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
@@ -73,7 +70,6 @@ export class PreferencesService {
|
||||
showThumbnails: updatedPreferences.showThumbnails,
|
||||
cacheMode: updatedPreferences.cacheMode as "memory" | "file",
|
||||
showOnlyUnread: updatedPreferences.showOnlyUnread,
|
||||
debug: updatedPreferences.debug,
|
||||
displayMode: updatedPreferences.displayMode as UserPreferences["displayMode"],
|
||||
background: updatedPreferences.background as unknown as BackgroundPreferences,
|
||||
};
|
||||
|
||||
@@ -14,8 +14,10 @@ class RequestQueue {
|
||||
private activeCount = 0;
|
||||
private maxConcurrent: number;
|
||||
|
||||
constructor(maxConcurrent: number = 5) {
|
||||
this.maxConcurrent = maxConcurrent;
|
||||
constructor(maxConcurrent?: number) {
|
||||
// Lire depuis env ou utiliser la valeur par défaut
|
||||
const envValue = process.env.KOMGA_MAX_CONCURRENT_REQUESTS;
|
||||
this.maxConcurrent = maxConcurrent ?? (envValue ? parseInt(envValue, 10) : 5);
|
||||
}
|
||||
|
||||
async enqueue<T>(execute: () => Promise<T>): Promise<T> {
|
||||
@@ -68,6 +70,10 @@ class RequestQueue {
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance - Limite à 2 requêtes simultanées vers Komga (réduit pour CPU)
|
||||
export const RequestQueueService = new RequestQueue(2);
|
||||
// Singleton instance - Par défaut limite à 2 requêtes simultanées (configurable via KOMGA_MAX_CONCURRENT_REQUESTS)
|
||||
export const RequestQueueService = new RequestQueue(
|
||||
process.env.KOMGA_MAX_CONCURRENT_REQUESTS
|
||||
? parseInt(process.env.KOMGA_MAX_CONCURRENT_REQUESTS, 10)
|
||||
: 2
|
||||
);
|
||||
|
||||
|
||||
@@ -92,6 +92,9 @@ export class SeriesService extends BaseApiService {
|
||||
// Filtrer les livres
|
||||
let filteredBooks = allBooks;
|
||||
|
||||
// Filtrer les livres supprimés (fichiers manquants sur le filesystem)
|
||||
filteredBooks = filteredBooks.filter((book: KomgaBook) => !book.deleted);
|
||||
|
||||
if (unreadOnly) {
|
||||
filteredBooks = filteredBooks.filter(
|
||||
(book: KomgaBook) => !book.readProgress || !book.readProgress.completed
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { PreferencesService } from "./preferences.service";
|
||||
import { DebugService } from "./debug.service";
|
||||
import { getCurrentUser } from "../auth-utils";
|
||||
|
||||
export type CacheMode = "file" | "memory";
|
||||
@@ -440,14 +439,13 @@ class ServerCacheService {
|
||||
const { data, isStale } = cachedResult;
|
||||
const endTime = performance.now();
|
||||
|
||||
// Log la requête avec l'indication du cache
|
||||
await DebugService.logRequest({
|
||||
url: `[CACHE${isStale ? '-STALE' : ''}] ${key}`,
|
||||
startTime,
|
||||
endTime,
|
||||
fromCache: true,
|
||||
cacheType: type,
|
||||
});
|
||||
// Debug logging
|
||||
if (process.env.CACHE_DEBUG === 'true') {
|
||||
const icon = isStale ? '⚠️' : '✅';
|
||||
const status = isStale ? 'STALE' : 'HIT';
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`${icon} [CACHE ${status}] ${key} | ${type} | ${(endTime - startTime).toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// Si le cache est expiré, revalider en background sans bloquer la réponse
|
||||
if (isStale) {
|
||||
@@ -459,9 +457,21 @@ class ServerCacheService {
|
||||
}
|
||||
|
||||
// Pas de cache du tout, fetch normalement
|
||||
if (process.env.CACHE_DEBUG === 'true') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`❌ [CACHE MISS] ${key} | ${type}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fetcher();
|
||||
this.set(cacheKey, data, type);
|
||||
|
||||
const endTime = performance.now();
|
||||
if (process.env.CACHE_DEBUG === 'true') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`💾 [CACHE SET] ${key} | ${type} | ${(endTime - startTime).toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -482,16 +492,13 @@ class ServerCacheService {
|
||||
const data = await fetcher();
|
||||
this.set(cacheKey, data, type);
|
||||
|
||||
const endTime = performance.now();
|
||||
await DebugService.logRequest({
|
||||
url: `[REVALIDATE] ${debugKey}`,
|
||||
startTime,
|
||||
endTime,
|
||||
fromCache: false,
|
||||
cacheType: type,
|
||||
});
|
||||
if (process.env.CACHE_DEBUG === 'true') {
|
||||
const endTime = performance.now();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`🔄 [CACHE REVALIDATE] ${debugKey} | ${type} | ${(endTime - startTime).toFixed(2)}ms`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Background revalidation failed for ${debugKey}:`, error);
|
||||
console.error(`🔴 [CACHE REVALIDATE ERROR] ${debugKey}:`, error);
|
||||
// Ne pas relancer l'erreur car c'est en background
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
// Wrapper pour détecter le cache du navigateur
|
||||
export async function fetchWithCacheDetection(url: string, options: RequestInit = {}) {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const endTime = performance.now();
|
||||
|
||||
// Détecter si la réponse vient du cache du navigateur
|
||||
const fromBrowserCache = response.headers.get('x-cache') === 'HIT' ||
|
||||
response.headers.get('cf-cache-status') === 'HIT' ||
|
||||
(endTime - startTime) < 5; // Si très rapide, probablement du cache
|
||||
|
||||
// Logger la requête seulement si ce n'est pas une requête de debug
|
||||
// Note: La vérification du mode debug se fait côté serveur dans DebugService
|
||||
if (!url.includes('/api/debug')) {
|
||||
try {
|
||||
await fetch("/api/debug", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
url: url,
|
||||
startTime,
|
||||
endTime,
|
||||
fromCache: fromBrowserCache,
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
// Ignorer les erreurs de logging
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
|
||||
// Logger aussi les erreurs seulement si ce n'est pas une requête de debug
|
||||
if (!url.includes('/api/debug')) {
|
||||
try {
|
||||
await fetch("/api/debug", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
url: url,
|
||||
startTime,
|
||||
endTime,
|
||||
fromCache: false,
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
// Ignorer les erreurs de logging
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { CacheType } from "./cache";
|
||||
|
||||
export interface RequestTiming {
|
||||
url: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
duration: number;
|
||||
timestamp: string;
|
||||
fromCache: boolean;
|
||||
cacheType?: CacheType;
|
||||
mongoAccess?: {
|
||||
operation: string;
|
||||
duration: number;
|
||||
};
|
||||
pageRender?: {
|
||||
page: string;
|
||||
duration: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ export interface KomgaBook {
|
||||
media: BookMedia;
|
||||
metadata: BookMetadata;
|
||||
readProgress: ReadProgress | null;
|
||||
deleted: boolean;
|
||||
}
|
||||
|
||||
export interface BookMedia {
|
||||
|
||||
@@ -10,7 +10,6 @@ export interface UserPreferences {
|
||||
showThumbnails: boolean;
|
||||
cacheMode: "memory" | "file";
|
||||
showOnlyUnread: boolean;
|
||||
debug: boolean;
|
||||
displayMode: {
|
||||
compact: boolean;
|
||||
itemsPerPage: number;
|
||||
@@ -22,7 +21,6 @@ export const defaultPreferences: UserPreferences = {
|
||||
showThumbnails: true,
|
||||
cacheMode: "memory",
|
||||
showOnlyUnread: false,
|
||||
debug: false,
|
||||
displayMode: {
|
||||
compact: false,
|
||||
itemsPerPage: 20,
|
||||
|
||||
23
src/utils/image-errors.ts
Normal file
23
src/utils/image-errors.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AppError } from "./errors";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
|
||||
/**
|
||||
* Helper pour trouver le status HTTP dans la chaîne d'erreurs imbriquées
|
||||
* Parcourt récursivement les originalError pour trouver une erreur KOMGA.HTTP_ERROR
|
||||
*/
|
||||
export function findHttpStatus(error: unknown): number | null {
|
||||
if (!(error instanceof AppError)) return null;
|
||||
|
||||
// Si c'est une erreur HTTP, récupérer le status
|
||||
if (error.code === ERROR_CODES.KOMGA.HTTP_ERROR) {
|
||||
return (error.params as any)?.status || null;
|
||||
}
|
||||
|
||||
// Sinon, chercher récursivement dans originalError
|
||||
if (error.originalError) {
|
||||
return findHttpStatus(error.originalError);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user