diff --git a/docs/api-get-cleanup.md b/docs/api-get-cleanup.md
index 0076105..bef6dac 100644
--- a/docs/api-get-cleanup.md
+++ b/docs/api-get-cleanup.md
@@ -1,63 +1,67 @@
-# Plan - Suppression des routes API GET restantes
-
-## État actuel
-
-Routes GET encore présentes mais peu/n'utilisées :
-
-| Route | Utilisation actuelle | Action |
-|-------|---------------------|--------|
-| `GET /api/komga/config` | ❌ Non utilisée | 🔴 Supprimer |
-| `GET /api/komga/favorites` | Sidebar, SeriesHeader (client) | 🟡 Optimiser |
-| `GET /api/preferences` | PreferencesContext (client) | 🟡 Optimiser |
-| `GET /api/komga/books/[bookId]` | ClientBookPage, DownloadManager | 🟡 Supprimer (données déjà en props) |
-| `GET /api/user/profile` | ? | 🔍 Vérifier |
-
-## Actions proposées
-
-### 1. Supprimer `GET /api/komga/config`
-
-La config est déjà appelée directement dans `settings/page.tsx` via `ConfigDBService.getConfig()`.
-
-**Action** : Supprimer la route API.
-
+---
+status: reviewed
+reviewed_at: 2026-02-28
+review_file: thoughts/reviews/api-get-cleanup-review.md
---
-### 2. Optimiser les préférences
+# Plan - Cleanup des routes API GET (focus RSC)
-Les préférences sont déjà passées depuis `layout.tsx` via `PreferencesService.getPreferences()`.
-Le `PreferencesContext` refetch en client - c'est redondant.
+## État réel (scan `src/app/api`)
-**Action** : Le contexte utilise déjà les `initialPreferences`. Le fetch client n'est nécessaire que si on n'a pas les données initiales.
+Routes GET actuellement présentes :
----
+### A. Migrees en Lot 1 (RSC, routes supprimees)
-### 3. Supprimer `GET /api/komga/books/[bookId]`
+| Route | Utilisation client actuelle | Cible | Action |
+|-------|-----------------------------|-------|--------|
+| `GET /api/preferences` | `src/contexts/PreferencesContext.tsx` | Préférences fournies par layout/page server | ✅ Supprimée |
+| `GET /api/komga/favorites` | `src/components/layout/Sidebar.tsx`, `src/components/series/SeriesHeader.tsx` | Favoris passés depuis parent Server Component | ✅ Supprimée |
+| `GET /api/admin/users` | `src/components/admin/AdminContent.tsx` | Page admin en RSC + props | ✅ Supprimée |
+| `GET /api/admin/stats` | `src/components/admin/AdminContent.tsx` | Page admin en RSC + props | ✅ Supprimée |
+| `GET /api/komga/libraries` | `src/components/settings/BackgroundSettings.tsx` | Données passées depuis page/settings server | ✅ Supprimée |
-Regardons ce que fait `ClientBookPage` :
+### B. A garder temporairement (interaction client forte)
-```tsx
-// Server Component (page.tsx) fetch les données
-const data = await BookService.getBook(bookId);
+| Route | Utilisation actuelle | Pourquoi garder maintenant | Piste de simplification |
+|-------|----------------------|----------------------------|-------------------------|
+| `GET /api/komga/libraries/[libraryId]/series` | `src/app/libraries/[libraryId]/LibraryClientWrapper.tsx` (pagination/filtre/recherche) | Navigation dynamique pilotée par query params côté client | Évaluer migration partielle vers navigation server (`searchParams`) |
+| `GET /api/komga/series/[seriesId]/books` | `src/app/series/[seriesId]/SeriesClientWrapper.tsx` (pagination/filtre) | Même contrainte de pagination dynamique | Même stratégie: server-first puis interactions ciblées |
+| `GET /api/komga/random-book` | `src/components/layout/ClientLayout.tsx` | Action utilisateur ponctuelle (random) | Option: server action dédiée plutôt qu'API GET |
+| `GET /api/komga/books/[bookId]` | fallback dans `ClientBookPage.tsx`, usage `DownloadManager.tsx` | fallback utile hors flux page SSR | Limiter au fallback strict, éviter le double-fetch |
+| `GET /api/komga/series/[seriesId]` | utilisé via Sidebar pour enrichir les favoris | enrichissement client en cascade | Charger les métadonnées nécessaires en amont côté server |
+| `GET /api/user/profile` | pas d'appel client direct trouvé | route utile pour consommation API interne/outils | Vérifier si remplaçable par service server direct |
+| `GET /api/komga/home` | endpoint de données agrégées | peut rester tant que la page consomme un service centralisé | privilégier appel server direct depuis page/home |
-// Passe à ClientBookPage
-
+### C. A conserver (API de transport / framework)
-// ClientClientBookPage refetch en client si pas de initialData
-useEffect(() => {
- if (!initialData) fetchBookData(); // Only if SSR failed
-}, [bookId, initialData]);
-```
+| Route | Raison |
+|-------|--------|
+| `GET /api/komga/images/**` | streaming/binaire image, adapté à une route API |
+| `GET /api/komga/books/[bookId]/pages/[pageNumber]` | endpoint image avec déduplication/cache |
+| `GET /api/auth/[...nextauth]` | handler NextAuth, à conserver |
-**Action** : Supprimer le fetch client - les données sont déjà en props.
+## Points importants
----
+- `GET /api/komga/config` n'existe plus dans `src/app/api` (déjà retirée).
+- Le gain principal vient des écrans qui refetchent des données déjà disponibles côté server (layout/page).
+- Objectif: réduire les GET API utilisés comme couche interne entre composants React et services serveur.
-### 4. Garder pour l'instant
+## Plan d'exécution recommandé
-Ces routes nécessitent plus de refactoring :
+1. **Lot 1 (quick wins)**
+ - Migrer `preferences`, `favorites`, `admin/users`, `admin/stats`, `komga/libraries` vers un chargement server-first.
+ - Garder les routes GET le temps de la transition, puis supprimer les appels client.
-- `GET /api/komga/favorites` - Utilisé dans des composants clients (Sidebar)
-- `GET /api/admin/users` - AdminContent
-- `GET /api/admin/stats` - AdminContent
+2. **Lot 2 (pages paginées)**
+ - Repenser `libraries/[libraryId]/series` et `series/[seriesId]/books` pour un flux `searchParams` server-first.
+ - Conserver seulement les interactions client réellement nécessaires.
-Ces cas pourraient être résolus en passant les données depuis des Server Components parents.
+3. **Lot 3 (stabilisation)**
+ - Vérifier `user/profile` et `komga/home` (route API vs appel direct service).
+ - Supprimer les routes GET devenues sans consommateurs.
+
+## Check de validation
+
+- Plus de `fetch("/api/...")` GET dans les composants server-capables.
+- Pas de régression UX sur pagination/filtres et random book.
+- Journal clair des routes supprimées et des routes conservées avec justification.
diff --git a/src/app/actions/admin.ts b/src/app/actions/admin.ts
index 6645803..c6c2203 100644
--- a/src/app/actions/admin.ts
+++ b/src/app/actions/admin.ts
@@ -1,10 +1,36 @@
"use server";
import { AdminService } from "@/lib/services/admin.service";
-import { ERROR_CODES } from "@/constants/errorCodes";
+import type { AdminUserData } from "@/lib/services/admin.service";
import { AppError } from "@/utils/errors";
import { AuthServerService } from "@/lib/services/auth-server.service";
+export interface AdminStatsData {
+ totalUsers: number;
+ totalAdmins: number;
+ usersWithKomga: number;
+ usersWithPreferences: number;
+}
+
+export async function getAdminDashboardData(): Promise<{
+ success: boolean;
+ users?: AdminUserData[];
+ stats?: AdminStatsData;
+ message?: string;
+}> {
+ try {
+ const [users, stats] = await Promise.all([AdminService.getAllUsers(), AdminService.getUserStats()]);
+
+ return { success: true, users, stats };
+ } catch (error) {
+ if (error instanceof AppError) {
+ return { success: false, message: error.message };
+ }
+
+ return { success: false, message: "Erreur lors de la récupération des données admin" };
+ }
+}
+
/**
* Met à jour les rôles d'un utilisateur
*/
diff --git a/src/app/api/admin/stats/route.ts b/src/app/api/admin/stats/route.ts
deleted file mode 100644
index 0e3cb07..0000000
--- a/src/app/api/admin/stats/route.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { NextResponse } from "next/server";
-import { AdminService } from "@/lib/services/admin.service";
-import { AppError } from "@/utils/errors";
-import logger from "@/lib/logger";
-
-export async function GET() {
- try {
- const stats = await AdminService.getUserStats();
- return NextResponse.json(stats);
- } catch (error) {
- logger.error({ err: error }, "Erreur lors de la récupération des stats:");
-
- if (error instanceof AppError) {
- return NextResponse.json(
- { error: error.message, code: error.code },
- {
- status:
- error.code === "AUTH_FORBIDDEN"
- ? 403
- : error.code === "AUTH_UNAUTHENTICATED"
- ? 401
- : 500,
- }
- );
- }
-
- return NextResponse.json(
- { error: "Erreur lors de la récupération des stats" },
- { status: 500 }
- );
- }
-}
diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts
deleted file mode 100644
index 7655ca1..0000000
--- a/src/app/api/admin/users/route.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { NextResponse } from "next/server";
-import { AdminService } from "@/lib/services/admin.service";
-import { AppError } from "@/utils/errors";
-import logger from "@/lib/logger";
-
-export async function GET() {
- try {
- const users = await AdminService.getAllUsers();
- return NextResponse.json(users);
- } catch (error) {
- logger.error({ err: error }, "Erreur lors de la récupération des utilisateurs:");
-
- if (error instanceof AppError) {
- return NextResponse.json(
- { error: error.message, code: error.code },
- {
- status:
- error.code === "AUTH_FORBIDDEN"
- ? 403
- : error.code === "AUTH_UNAUTHENTICATED"
- ? 401
- : 500,
- }
- );
- }
-
- return NextResponse.json(
- { error: "Erreur lors de la récupération des utilisateurs" },
- { status: 500 }
- );
- }
-}
diff --git a/src/app/api/komga/favorites/route.ts b/src/app/api/komga/favorites/route.ts
deleted file mode 100644
index de4b4a1..0000000
--- a/src/app/api/komga/favorites/route.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { NextResponse } from "next/server";
-import { FavoriteService } from "@/lib/services/favorite.service";
-import { SeriesService } from "@/lib/services/series.service";
-import { ERROR_CODES } from "@/constants/errorCodes";
-import { AppError } from "@/utils/errors";
-import { getErrorMessage } from "@/utils/errors";
-import logger from "@/lib/logger";
-
-// GET reste utilisé par Sidebar et SeriesHeader pour récupérer la liste des favoris
-export async function GET() {
- try {
- const favoriteIds: string[] = await FavoriteService.getAllFavoriteIds();
-
- // Valider que chaque série existe encore dans Komga
- const validFavoriteIds: string[] = [];
-
- for (const seriesId of favoriteIds) {
- try {
- await SeriesService.getSeries(seriesId);
- validFavoriteIds.push(seriesId);
- } catch {
- // Si la série n'existe plus dans Komga, on la retire des favoris
- try {
- await FavoriteService.removeFromFavorites(seriesId);
- } catch {
- // Erreur silencieuse, la série reste dans les favoris
- }
- }
- }
-
- return NextResponse.json(validFavoriteIds);
- } catch (error) {
- if (error instanceof AppError) {
- // Si la config Komga n'existe pas, retourner un tableau vide au lieu d'une erreur
- if (error.code === ERROR_CODES.KOMGA.MISSING_CONFIG) {
- return NextResponse.json([]);
- }
- }
- logger.error({ err: error }, "Erreur lors de la récupération des favoris:");
- if (error instanceof AppError) {
- return NextResponse.json(
- {
- error: {
- code: error.code,
- name: "Favorite fetch error",
- message: getErrorMessage(error.code),
- },
- },
- { status: 500 }
- );
- }
- return NextResponse.json(
- {
- error: {
- code: ERROR_CODES.FAVORITE.FETCH_ERROR,
- name: "Favorite fetch error",
- message: getErrorMessage(ERROR_CODES.FAVORITE.FETCH_ERROR),
- },
- },
- { status: 500 }
- );
- }
-}
diff --git a/src/app/api/komga/libraries/route.ts b/src/app/api/komga/libraries/route.ts
deleted file mode 100644
index c55670d..0000000
--- a/src/app/api/komga/libraries/route.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { NextResponse } from "next/server";
-import { LibraryService } from "@/lib/services/library.service";
-import { ERROR_CODES } from "@/constants/errorCodes";
-import { AppError } from "@/utils/errors";
-import type { KomgaLibrary } from "@/types/komga";
-import { getErrorMessage } from "@/utils/errors";
-import logger from "@/lib/logger";
-
-// Cache handled in service via fetchFromApi options
-
-export async function GET() {
- try {
- const libraries: KomgaLibrary[] = await LibraryService.getLibraries();
- return NextResponse.json(libraries);
- } catch (error) {
- if (error instanceof AppError) {
- // Si la config Komga n'existe pas, retourner un tableau vide au lieu d'une erreur
- if (error.code === ERROR_CODES.KOMGA.MISSING_CONFIG) {
- return NextResponse.json([]);
- }
- }
- logger.error({ err: error }, "API Libraries - Erreur:");
- if (error instanceof AppError) {
- return NextResponse.json(
- {
- error: {
- code: error.code,
- name: "Library fetch error",
- message: getErrorMessage(error.code),
- },
- },
- { status: 500 }
- );
- }
- return NextResponse.json(
- {
- error: {
- code: ERROR_CODES.LIBRARY.FETCH_ERROR,
- name: "Library fetch error",
- message: getErrorMessage(ERROR_CODES.LIBRARY.FETCH_ERROR),
- },
- },
- { status: 500 }
- );
- }
-}
diff --git a/src/app/api/preferences/route.ts b/src/app/api/preferences/route.ts
deleted file mode 100644
index 7cae517..0000000
--- a/src/app/api/preferences/route.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { NextRequest } from "next/server";
-import { NextResponse } from "next/server";
-import { PreferencesService } from "@/lib/services/preferences.service";
-import { ERROR_CODES } from "@/constants/errorCodes";
-import { AppError } from "@/utils/errors";
-import type { UserPreferences } from "@/types/preferences";
-import { getErrorMessage } from "@/utils/errors";
-import logger from "@/lib/logger";
-
-// GET reste utilisé par PreferencesContext pour récupérer les préférences
-export async function GET() {
- try {
- const preferences: UserPreferences = await PreferencesService.getPreferences();
- return NextResponse.json(preferences);
- } catch (error) {
- logger.error({ err: error }, "Erreur lors de la récupération des préférences:");
- if (error instanceof AppError) {
- return NextResponse.json(
- {
- error: {
- name: "Preferences fetch error",
- code: error.code,
- message: getErrorMessage(error.code),
- },
- },
- { status: 500 }
- );
- }
- return NextResponse.json(
- {
- error: {
- name: "Preferences fetch error",
- code: ERROR_CODES.PREFERENCES.FETCH_ERROR,
- message: getErrorMessage(ERROR_CODES.PREFERENCES.FETCH_ERROR),
- },
- },
- { status: 500 }
- );
- }
-}
diff --git a/src/app/series/[seriesId]/SeriesContent.tsx b/src/app/series/[seriesId]/SeriesContent.tsx
index 51ef8fd..d109949 100644
--- a/src/app/series/[seriesId]/SeriesContent.tsx
+++ b/src/app/series/[seriesId]/SeriesContent.tsx
@@ -15,6 +15,7 @@ interface SeriesContentProps {
preferences: UserPreferences;
unreadOnly: boolean;
pageSize: number;
+ initialIsFavorite: boolean;
}
export function SeriesContent({
@@ -23,6 +24,7 @@ export function SeriesContent({
currentPage,
preferences,
unreadOnly,
+ initialIsFavorite,
}: SeriesContentProps) {
const { refreshSeries } = useRefresh();
@@ -31,6 +33,7 @@ export function SeriesContent({
({ success: false }))}
+ initialIsFavorite={initialIsFavorite}
/>
);
}
-
diff --git a/src/app/series/[seriesId]/page.tsx b/src/app/series/[seriesId]/page.tsx
index 217b005..dc96a82 100644
--- a/src/app/series/[seriesId]/page.tsx
+++ b/src/app/series/[seriesId]/page.tsx
@@ -1,5 +1,6 @@
import { PreferencesService } from "@/lib/services/preferences.service";
import { SeriesService } from "@/lib/services/series.service";
+import { FavoriteService } from "@/lib/services/favorite.service";
import { SeriesClientWrapper } from "./SeriesClientWrapper";
import { SeriesContent } from "./SeriesContent";
import { ErrorMessage } from "@/components/ui/ErrorMessage";
@@ -28,9 +29,10 @@ export default async function SeriesPage({ params, searchParams }: PageProps) {
const effectivePageSize = size ? parseInt(size) : preferences.displayMode?.itemsPerPage || DEFAULT_PAGE_SIZE;
try {
- const [books, series] = await Promise.all([
+ const [books, series, isFavorite] = await Promise.all([
SeriesService.getSeriesBooks(seriesId, currentPage - 1, effectivePageSize, unreadOnly),
SeriesService.getSeries(seriesId),
+ FavoriteService.isFavorite(seriesId),
]);
return (
@@ -48,6 +50,7 @@ export default async function SeriesPage({ params, searchParams }: PageProps) {
preferences={preferences}
unreadOnly={unreadOnly}
pageSize={effectivePageSize}
+ initialIsFavorite={isFavorite}
/>
);
diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx
index 72def25..fbbbce4 100644
--- a/src/app/settings/page.tsx
+++ b/src/app/settings/page.tsx
@@ -1,7 +1,8 @@
import { ConfigDBService } from "@/lib/services/config-db.service";
+import { LibraryService } from "@/lib/services/library.service";
import { ClientSettings } from "@/components/settings/ClientSettings";
import type { Metadata } from "next";
-import type { KomgaConfig } from "@/types/komga";
+import type { KomgaConfig, KomgaLibrary } from "@/types/komga";
import logger from "@/lib/logger";
export const dynamic = "force-dynamic";
@@ -13,6 +14,7 @@ export const metadata: Metadata = {
export default async function SettingsPage() {
let config: KomgaConfig | null = null;
+ let libraries: KomgaLibrary[] = [];
try {
// Récupérer la configuration Komga
@@ -26,10 +28,12 @@ export default async function SettingsPage() {
password: null,
};
}
+
+ libraries = await LibraryService.getLibraries();
} catch (error) {
logger.error({ err: error }, "Erreur lors de la récupération de la configuration:");
// On ne fait rien si la config n'existe pas, on laissera le composant client gérer l'état initial
}
- return ;
+ return ;
}
diff --git a/src/components/admin/AdminContent.tsx b/src/components/admin/AdminContent.tsx
index cfcaf26..fb4f610 100644
--- a/src/components/admin/AdminContent.tsx
+++ b/src/components/admin/AdminContent.tsx
@@ -1,21 +1,17 @@
"use client";
-import { useState, useCallback } from "react";
+import { useState, useCallback, useEffect } from "react";
import type { AdminUserData } from "@/lib/services/admin.service";
import { StatsCards } from "./StatsCards";
import { UsersTable } from "./UsersTable";
import { Button } from "@/components/ui/button";
import { RefreshCw } from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
+import { getAdminDashboardData, type AdminStatsData } from "@/app/actions/admin";
interface AdminContentProps {
initialUsers: AdminUserData[];
- initialStats: {
- totalUsers: number;
- totalAdmins: number;
- usersWithKomga: number;
- usersWithPreferences: number;
- };
+ initialStats: AdminStatsData;
}
export function AdminContent({ initialUsers, initialStats }: AdminContentProps) {
@@ -24,22 +20,25 @@ export function AdminContent({ initialUsers, initialStats }: AdminContentProps)
const [isRefreshing, setIsRefreshing] = useState(false);
const { toast } = useToast();
+ useEffect(() => {
+ setUsers(initialUsers);
+ }, [initialUsers]);
+
+ useEffect(() => {
+ setStats(initialStats);
+ }, [initialStats]);
+
const refreshData = useCallback(async () => {
setIsRefreshing(true);
try {
- const [usersResponse, statsResponse] = await Promise.all([
- fetch("/api/admin/users"),
- fetch("/api/admin/stats"),
- ]);
+ const result = await getAdminDashboardData();
- if (!usersResponse.ok || !statsResponse.ok) {
+ if (!result.success || !result.users || !result.stats) {
throw new Error("Erreur lors du rafraîchissement");
}
- const [newUsers, newStats] = await Promise.all([usersResponse.json(), statsResponse.json()]);
-
- setUsers(newUsers);
- setStats(newStats);
+ setUsers(result.users);
+ setStats(result.stats);
toast({
title: "Données rafraîchies",
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
index 8bb6b7f..a2bf14f 100644
--- a/src/components/layout/Sidebar.tsx
+++ b/src/components/layout/Sidebar.tsx
@@ -16,9 +16,6 @@ import { cn } from "@/lib/utils";
import { signOut } from "next-auth/react";
import { useEffect, useState, useCallback } from "react";
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
-import { AppError } from "@/utils/errors";
-import { ERROR_CODES } from "@/constants/errorCodes";
-import { getErrorMessage } from "@/utils/errors";
import { useToast } from "@/components/ui/use-toast";
import { useTranslate } from "@/hooks/useTranslate";
import { NavButton } from "@/components/ui/nav-button";
@@ -49,73 +46,42 @@ export function Sidebar({
const { toast } = useToast();
- const refreshFavorites = useCallback(async () => {
- try {
- const favoritesResponse = await fetch("/api/komga/favorites");
- if (!favoritesResponse.ok) {
- throw new AppError(ERROR_CODES.FAVORITE.FETCH_ERROR);
- }
- const favoriteIds = await favoritesResponse.json();
+ useEffect(() => {
+ setLibraries(initialLibraries || []);
+ }, [initialLibraries]);
- if (favoriteIds.length === 0) {
- setFavorites([]);
- return;
- }
-
- const promises = favoriteIds.map(async (id: string) => {
- const response = await fetch(`/api/komga/series/${id}`);
- if (!response.ok) {
- throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR);
- }
- return response.json();
- });
-
- const results = await Promise.all(promises);
- setFavorites(results.filter((series): series is KomgaSeries => series !== null));
- } catch (error) {
- logger.error({ err: error }, "Erreur de chargement des favoris:");
- toast({
- title: "Erreur",
- description:
- error instanceof AppError
- ? error.message
- : getErrorMessage(ERROR_CODES.FAVORITE.FETCH_ERROR),
- variant: "destructive",
- });
- }
- }, [toast]);
+ useEffect(() => {
+ setFavorites(initialFavorites || []);
+ }, [initialFavorites]);
// Mettre à jour les favoris quand ils changent (mise à jour optimiste)
useEffect(() => {
- const handleFavoritesChange = async (event: Event) => {
- const customEvent = event as CustomEvent<{ seriesId: string; action: "add" | "remove" }>;
+ const handleFavoritesChange = (event: Event) => {
+ const customEvent = event as CustomEvent<{
+ seriesId?: string;
+ action?: "add" | "remove";
+ series?: KomgaSeries;
+ }>;
// Si on a les détails de l'action, faire une mise à jour optimiste locale
if (customEvent.detail?.seriesId) {
- const { seriesId, action } = customEvent.detail;
+ const { seriesId, action, series } = customEvent.detail;
- if (action === "add") {
- // Fetch les détails de la série ajoutée et l'ajouter au state
- try {
- const response = await fetch(`/api/komga/series/${seriesId}`);
- if (response.ok) {
- const seriesData = await response.json();
- setFavorites((prev) => {
- // Éviter les doublons
- if (prev.some((s) => s.id === seriesId)) return prev;
- return [...prev, seriesData];
- });
+ if (action === "add" && series) {
+ setFavorites((prev) => {
+ if (prev.some((s) => s.id === series.id)) {
+ return prev;
}
- } catch (error) {
- logger.error({ err: error }, "Erreur lors de l'ajout optimiste du favori:");
- }
+
+ return [...prev, series];
+ });
} else if (action === "remove") {
- // Retirer la série du state directement
setFavorites((prev) => prev.filter((s) => s.id !== seriesId));
+ } else {
+ router.refresh();
}
} else {
- // Fallback: refetch complet si pas de détails (ex: événement externe)
- refreshFavorites();
+ router.refresh();
}
};
@@ -124,7 +90,7 @@ export function Sidebar({
return () => {
window.removeEventListener("favoritesChanged", handleFavoritesChange);
};
- }, [refreshFavorites]);
+ }, [router]);
const handleRefresh = async () => {
setIsRefreshing(true);
diff --git a/src/components/series/SeriesHeader.tsx b/src/components/series/SeriesHeader.tsx
index e7f2096..50439a4 100644
--- a/src/components/series/SeriesHeader.tsx
+++ b/src/components/series/SeriesHeader.tsx
@@ -18,37 +18,17 @@ import { addToFavorites, removeFromFavorites } from "@/app/actions/favorites";
interface SeriesHeaderProps {
series: KomgaSeries;
refreshSeries: (seriesId: string) => Promise<{ success: boolean; error?: string }>;
+ initialIsFavorite: boolean;
}
-export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {
+export const SeriesHeader = ({ series, refreshSeries, initialIsFavorite }: SeriesHeaderProps) => {
const { toast } = useToast();
- const [isFavorite, setIsFavorite] = useState(false);
+ const [isFavorite, setIsFavorite] = useState(initialIsFavorite);
const { t } = useTranslate();
useEffect(() => {
- const checkFavorite = async () => {
- try {
- const response = await fetch("/api/komga/favorites");
- if (!response.ok) {
- throw new AppError(ERROR_CODES.FAVORITE.STATUS_CHECK_ERROR);
- }
- const favoriteIds = await response.json();
- setIsFavorite(favoriteIds.includes(series.id));
- } catch (error) {
- logger.error({ err: error }, "Erreur lors de la vérification des favoris:");
- toast({
- title: "Erreur",
- description:
- error instanceof AppError
- ? error.message
- : getErrorMessage(ERROR_CODES.FAVORITE.NETWORK_ERROR),
- variant: "destructive",
- });
- }
- };
-
- checkFavorite();
- }, [series.id, toast]);
+ setIsFavorite(initialIsFavorite);
+ }, [series.id, initialIsFavorite]);
const handleToggleFavorite = async () => {
try {
@@ -59,7 +39,11 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {
setIsFavorite(!isFavorite);
// Dispatcher l'événement avec le seriesId pour mise à jour optimiste de la sidebar
const event = new CustomEvent("favoritesChanged", {
- detail: { seriesId: series.id, action: isFavorite ? "remove" : "add" },
+ detail: {
+ seriesId: series.id,
+ action: isFavorite ? "remove" : "add",
+ series: isFavorite ? undefined : series,
+ },
});
window.dispatchEvent(event);
toast({
diff --git a/src/components/settings/BackgroundSettings.tsx b/src/components/settings/BackgroundSettings.tsx
index 3805bb0..fe3cc99 100644
--- a/src/components/settings/BackgroundSettings.tsx
+++ b/src/components/settings/BackgroundSettings.tsx
@@ -17,34 +17,28 @@ import { SliderControl } from "@/components/ui/slider-control";
import type { KomgaLibrary } from "@/types/komga";
import logger from "@/lib/logger";
-export function BackgroundSettings() {
+interface BackgroundSettingsProps {
+ initialLibraries: KomgaLibrary[];
+}
+
+export function BackgroundSettings({ initialLibraries }: BackgroundSettingsProps) {
const { t } = useTranslate();
const { toast } = useToast();
const { preferences, updatePreferences } = usePreferences();
const [customImageUrl, setCustomImageUrl] = useState(preferences.background.imageUrl || "");
const [komgaConfigValid, setKomgaConfigValid] = useState(false);
- const [libraries, setLibraries] = useState([]);
+ const [libraries, setLibraries] = useState(initialLibraries || []);
const [selectedLibraries, setSelectedLibraries] = useState(
preferences.background.komgaLibraries || []
);
- // Vérifier la config Komga au chargement
useEffect(() => {
- const checkKomgaConfig = async () => {
- try {
- const response = await fetch("/api/komga/libraries");
- if (response.ok) {
- const libs = await response.json();
- setLibraries(libs);
- setKomgaConfigValid(libs.length > 0);
- }
- } catch (error) {
- logger.error({ err: error }, "Erreur lors de la vérification de la config Komga:");
- setKomgaConfigValid(false);
- }
- };
- checkKomgaConfig();
- }, []);
+ setLibraries(initialLibraries || []);
+ }, [initialLibraries]);
+
+ useEffect(() => {
+ setKomgaConfigValid(libraries.length > 0);
+ }, [libraries]);
const handleBackgroundTypeChange = async (type: BackgroundType) => {
try {
diff --git a/src/components/settings/ClientSettings.tsx b/src/components/settings/ClientSettings.tsx
index 2793312..bd00fe5 100644
--- a/src/components/settings/ClientSettings.tsx
+++ b/src/components/settings/ClientSettings.tsx
@@ -1,6 +1,7 @@
"use client";
import type { KomgaConfig } from "@/types/komga";
+import type { KomgaLibrary } from "@/types/komga";
import { useTranslate } from "@/hooks/useTranslate";
import { DisplaySettings } from "./DisplaySettings";
import { KomgaSettings } from "./KomgaSettings";
@@ -12,9 +13,10 @@ import { Monitor, Network } from "lucide-react";
interface ClientSettingsProps {
initialConfig: KomgaConfig | null;
+ initialLibraries: KomgaLibrary[];
}
-export function ClientSettings({ initialConfig }: ClientSettingsProps) {
+export function ClientSettings({ initialConfig, initialLibraries }: ClientSettingsProps) {
const { t } = useTranslate();
return (
@@ -35,7 +37,7 @@ export function ClientSettings({ initialConfig }: ClientSettingsProps) {
-
+
diff --git a/src/contexts/PreferencesContext.tsx b/src/contexts/PreferencesContext.tsx
index 33229f4..380aba6 100644
--- a/src/contexts/PreferencesContext.tsx
+++ b/src/contexts/PreferencesContext.tsx
@@ -17,10 +17,6 @@ interface PreferencesContextType {
const PreferencesContext = createContext(undefined);
-// Module-level flag to prevent duplicate fetches (survives StrictMode remounts)
-let preferencesFetchInProgress = false;
-let preferencesFetched = false;
-
export function PreferencesProvider({
children,
initialPreferences,
@@ -32,58 +28,23 @@ export function PreferencesProvider({
const [preferences, setPreferences] = useState(
initialPreferences || defaultPreferences
);
- const [isLoading, setIsLoading] = useState(false);
+ const isLoading = false;
// Check if we have valid initial preferences from server
const hasValidInitialPreferences =
initialPreferences && Object.keys(initialPreferences).length > 0;
- const fetchPreferences = useCallback(async () => {
- // Prevent concurrent fetches
- if (preferencesFetchInProgress || preferencesFetched) {
+ useEffect(() => {
+ if (status === "authenticated" && hasValidInitialPreferences) {
+ setPreferences(initialPreferences);
return;
}
- preferencesFetchInProgress = true;
- try {
- const response = await fetch("/api/preferences");
- if (!response.ok) {
- throw new AppError(ERROR_CODES.PREFERENCES.FETCH_ERROR);
- }
- const data = await response.json();
- setPreferences({
- ...defaultPreferences,
- ...data,
- displayMode: {
- ...defaultPreferences.displayMode,
- ...(data.displayMode || {}),
- viewMode: data.displayMode?.viewMode || defaultPreferences.displayMode.viewMode,
- },
- });
- preferencesFetched = true;
- } catch (error) {
- logger.error({ err: error }, "Erreur lors de la récupération des préférences");
- setPreferences(defaultPreferences);
- } finally {
- setIsLoading(false);
- preferencesFetchInProgress = false;
- }
- }, []);
-
- useEffect(() => {
- if (status === "authenticated") {
- // Skip refetch if we already have valid initial preferences from server
- if (hasValidInitialPreferences) {
- preferencesFetched = true; // Mark as fetched since we have server data
- return;
- }
- fetchPreferences();
- } else if (status === "unauthenticated") {
+ if (status === "unauthenticated") {
// Reset to defaults when user logs out
setPreferences(defaultPreferences);
- preferencesFetched = false; // Allow refetch on next login
}
- }, [status, fetchPreferences, hasValidInitialPreferences]);
+ }, [status, hasValidInitialPreferences, initialPreferences]);
const updatePreferences = useCallback(async (newPreferences: Partial): Promise => {
try {
diff --git a/thoughts/reviews/api-get-cleanup-review.md b/thoughts/reviews/api-get-cleanup-review.md
new file mode 100644
index 0000000..cc6ac35
--- /dev/null
+++ b/thoughts/reviews/api-get-cleanup-review.md
@@ -0,0 +1,52 @@
+## Validation Report: api-get-cleanup.md
+
+### Implementation Status
+- ✓ Lot 1 (quick wins) - Majoritairement implémente
+- ⚠️ Vérification automatique - Partielle (typecheck OK, lint KO)
+- ⚠️ Clôture de lot - Partielle (quelques écarts vs plan)
+
+### Automated Verification Results
+- ✓ Type checking passe: `pnpm typecheck`
+- ✗ Lint échoue: `pnpm lint`
+ - Erreur observee: `Invalid project directory provided, no such directory: /Users/julienfroidefond/Sites/stripstream/lint`
+ - Impact: impossible de valider la qualite lint via la commande standard du repo
+
+### Code Review Findings
+
+#### Matches Plan
+- `GET /api/preferences` retire du flux client dans `src/contexts/PreferencesContext.tsx` (plus de fetch XHR direct).
+- `GET /api/komga/favorites` retire du client dans `src/components/layout/Sidebar.tsx` et `src/components/series/SeriesHeader.tsx`.
+- `GET /api/admin/users` et `GET /api/admin/stats` remplaces par server action `getAdminDashboardData` dans `src/app/actions/admin.ts`, consommee par `src/components/admin/AdminContent.tsx`.
+- `GET /api/komga/libraries` retire du client settings via passage de donnees server-side (`src/app/settings/page.tsx` -> `src/components/settings/ClientSettings.tsx` -> `src/components/settings/BackgroundSettings.tsx`).
+
+#### Deviations from Plan
+- **Lot 1 / Preferences**: le plan mentionnait un fallback temporaire client pour les preferences; l'implementation supprime le fallback et repose uniquement sur les donnees server + reset logout.
+ - **Assessment**: deviation acceptable si `PreferencesService.getPreferences()` reste fiable pour tous les cas authentifies.
+ - **Recommendation**: confirmer en manuel le comportement apres login, refresh hard et reconnexion.
+
+#### Potential Issues
+- `pnpm lint` est actuellement non exploitable (script/tooling), donc la verification standard de style/regles n'est pas couverte.
+- La logique optimiste favoris en sidebar repose sur l'evenement `favoritesChanged` enrichi; le fallback `router.refresh()` couvre le cas sans detail, mais doit etre teste en navigation reelle.
+
+### Manual Testing Required
+1. Favoris (series + sidebar)
+ - [ ] Ajouter une serie en favori depuis la page serie, verifier apparition immediate en sidebar.
+ - [ ] Retirer une serie des favoris, verifier disparition immediate.
+ - [ ] Recharger la page, verifier persistance exacte des favoris.
+
+2. Preferences
+ - [ ] Modifier une preference (display/background), verifier persistance apres reload.
+ - [ ] Se deconnecter/reconnecter, verifier reset puis rechargement correct des preferences.
+
+3. Admin
+ - [ ] Ouvrir admin, verifier affichage users/stats initiaux.
+ - [ ] Cliquer "Rafraichir", verifier mise a jour sans appel XHR `/api/admin/*`.
+
+4. Settings libraries
+ - [ ] Ouvrir settings/display/background, verifier chargement bibliotheques Komga.
+ - [ ] Basculer sur fond `komga-random` et verifier la liste des bibliotheques.
+
+### Recommendations
+- Corriger la commande lint du projet pour retablir la verification automatique complete.
+- Ajouter (ou mettre a jour) un test d'integration pour le flux favoris optimiste (event + rerender sidebar).
+- En lot 2, conserver le meme principe server-first pour les routes paginees restantes.