From 29f5324bd72f8a5f022fb80e54fbda65e64b96d9 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sat, 28 Feb 2026 11:43:11 +0100 Subject: [PATCH] refactor: remove client-only GET API routes for lot 1 --- docs/api-get-cleanup.md | 98 ++++++++++--------- src/app/actions/admin.ts | 28 +++++- src/app/api/admin/stats/route.ts | 32 ------ src/app/api/admin/users/route.ts | 32 ------ src/app/api/komga/favorites/route.ts | 63 ------------ src/app/api/komga/libraries/route.ts | 46 --------- src/app/api/preferences/route.ts | 40 -------- src/app/series/[seriesId]/SeriesContent.tsx | 4 +- src/app/series/[seriesId]/page.tsx | 5 +- src/app/settings/page.tsx | 8 +- src/components/admin/AdminContent.tsx | 31 +++--- src/components/layout/Sidebar.tsx | 82 +++++----------- src/components/series/SeriesHeader.tsx | 36 ++----- .../settings/BackgroundSettings.tsx | 30 +++--- src/components/settings/ClientSettings.tsx | 6 +- src/contexts/PreferencesContext.tsx | 51 ++-------- thoughts/reviews/api-get-cleanup-review.md | 52 ++++++++++ 17 files changed, 214 insertions(+), 430 deletions(-) delete mode 100644 src/app/api/admin/stats/route.ts delete mode 100644 src/app/api/admin/users/route.ts delete mode 100644 src/app/api/komga/favorites/route.ts delete mode 100644 src/app/api/komga/libraries/route.ts delete mode 100644 src/app/api/preferences/route.ts create mode 100644 thoughts/reviews/api-get-cleanup-review.md 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.