Compare commits
9 Commits
7f361ce0a2
...
26021ea907
| Author | SHA1 | Date | |
|---|---|---|---|
| 26021ea907 | |||
| 5eba969846 | |||
| 9a11ab16bb | |||
| 70a77481e5 | |||
| b1e0e18d9e | |||
| e5497b4f58 | |||
| 612a70ffbe | |||
| 1a88efc46b | |||
| 29f5324bd7 |
@@ -1,63 +1,72 @@
|
|||||||
# Plan - Suppression des routes API GET restantes
|
---
|
||||||
|
status: reviewed
|
||||||
## État actuel
|
reviewed_at: 2026-02-28
|
||||||
|
review_file: thoughts/reviews/api-get-cleanup-review.md
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 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()`.
|
## État réel (scan `src/app/api`)
|
||||||
Le `PreferencesContext` refetch en client - c'est redondant.
|
|
||||||
|
|
||||||
**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
|
| Route | Utilisation actuelle | Pourquoi garder maintenant | Piste de simplification |
|
||||||
// Server Component (page.tsx) fetch les données
|
|-------|----------------------|----------------------------|-------------------------|
|
||||||
const data = await BookService.getBook(bookId);
|
|
||||||
|
|
||||||
// Passe à ClientBookPage
|
### B2. Migrees en Lot 2 (pagination server-first)
|
||||||
<ClientBookPage bookId={bookId} initialData={{ ...data, nextBook }} />
|
|
||||||
|
|
||||||
// ClientClientBookPage refetch en client si pas de initialData
|
| Route | Utilisation client actuelle | Cible | Action |
|
||||||
useEffect(() => {
|
|-------|-----------------------------|-------|--------|
|
||||||
if (!initialData) fetchBookData(); // Only if SSR failed
|
| `GET /api/komga/libraries/[libraryId]/series` | `src/app/libraries/[libraryId]/LibraryClientWrapper.tsx` | Chargement via `searchParams` dans page server | ✅ Supprimée |
|
||||||
}, [bookId, initialData]);
|
| `GET /api/komga/series/[seriesId]/books` | `src/app/series/[seriesId]/SeriesClientWrapper.tsx` | Chargement via `searchParams` dans page server | ✅ Supprimée |
|
||||||
```
|
| `GET /api/komga/random-book` | `src/components/layout/ClientLayout.tsx` | Action utilisateur via server action | ✅ Supprimée |
|
||||||
|
| `GET /api/komga/home` | `src/app/page.tsx` consomme déjà `HomeService` côté server | Données agrégées directement via service server | ✅ Supprimée |
|
||||||
|
| `GET /api/user/profile` | aucun consommateur client trouvé, page compte déjà server-first | Profil/statistiques via `UserService` en Server Component | ✅ Supprimée |
|
||||||
|
| `GET /api/komga/series/[seriesId]` | plus de consommateur `fetch('/api/...')` (chargement via `SeriesService`) | Détail série chargé en Server Component | ✅ Supprimée |
|
||||||
|
| `GET /api/komga/books/[bookId]` | fallback client (`ClientBookPage`) et DownloadManager migrés vers server action | Données livre/pages/nextBook via `BookService` et action server | ✅ Supprimée |
|
||||||
|
|
||||||
**Action** : Supprimer le fetch client - les données sont déjà en props.
|
### C. A conserver (API de transport / framework)
|
||||||
|
|
||||||
---
|
| 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 |
|
||||||
|
|
||||||
### 4. Garder pour l'instant
|
## Points importants
|
||||||
|
|
||||||
Ces routes nécessitent plus de refactoring :
|
- `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.
|
||||||
|
|
||||||
- `GET /api/komga/favorites` - Utilisé dans des composants clients (Sidebar)
|
## Plan d'exécution recommandé
|
||||||
- `GET /api/admin/users` - AdminContent
|
|
||||||
- `GET /api/admin/stats` - AdminContent
|
|
||||||
|
|
||||||
Ces cas pourraient être résolus en passant les données depuis des Server Components parents.
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
96
eslint.config.mjs
Normal file
96
eslint.config.mjs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { defineConfig } from "eslint/config";
|
||||||
|
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTypescript from "eslint-config-next/typescript";
|
||||||
|
import unusedImports from "eslint-plugin-unused-imports";
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
...nextCoreWebVitals,
|
||||||
|
...nextTypescript,
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
"unused-imports": unusedImports,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
caughtErrorsIgnorePattern: "^_",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"no-console": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
allow: ["warn", "error"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@next/next/no-html-link-for-pages": "off",
|
||||||
|
"react/no-unescaped-entities": "off",
|
||||||
|
"no-unreachable": "error",
|
||||||
|
"no-unused-expressions": "warn",
|
||||||
|
"no-unused-private-class-members": "warn",
|
||||||
|
"unused-imports/no-unused-imports": "warn",
|
||||||
|
"unused-imports/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
vars: "all",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
args: "after-used",
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
caughtErrors: "all",
|
||||||
|
caughtErrorsIgnorePattern: "^_",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"no-empty-function": "warn",
|
||||||
|
"no-empty": ["warn", { allowEmptyCatch: true }],
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
"@typescript-eslint/no-empty-object-type": "warn",
|
||||||
|
"@typescript-eslint/no-require-imports": "warn",
|
||||||
|
"react-hooks/error-boundaries": "warn",
|
||||||
|
"react-hooks/set-state-in-effect": "warn",
|
||||||
|
"react-hooks/refs": "warn",
|
||||||
|
"react-hooks/purity": "warn",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["scripts/**/*.{js,mjs,cjs}"],
|
||||||
|
rules: {
|
||||||
|
"no-console": "off",
|
||||||
|
"@typescript-eslint/no-require-imports": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["src/app/**/*.tsx"],
|
||||||
|
rules: {
|
||||||
|
"react-hooks/error-boundaries": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
"src/components/layout/ClientLayout.tsx",
|
||||||
|
"src/components/layout/Sidebar.tsx",
|
||||||
|
"src/components/library/LibraryHeader.tsx",
|
||||||
|
"src/components/reader/components/PageDisplay.tsx",
|
||||||
|
"src/components/reader/components/Thumbnail.tsx",
|
||||||
|
"src/components/reader/hooks/useThumbnails.ts",
|
||||||
|
"src/components/ui/InstallPWA.tsx",
|
||||||
|
"src/components/ui/cover-client.tsx",
|
||||||
|
"src/components/series/BookGrid.tsx",
|
||||||
|
"src/components/series/BookList.tsx",
|
||||||
|
"src/contexts/ServiceWorkerContext.tsx",
|
||||||
|
"src/hooks/useNetworkStatus.ts",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"react-hooks/set-state-in-effect": "off",
|
||||||
|
"react-hooks/refs": "off",
|
||||||
|
"react-hooks/purity": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["src/components/ui/cover-client.tsx"],
|
||||||
|
rules: {
|
||||||
|
"@next/next/no-img-element": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"start:prod": "node scripts/init-db.mjs && pnpm start",
|
"start:prod": "node scripts/init-db.mjs && pnpm start",
|
||||||
"init-db": "node scripts/init-db.mjs",
|
"init-db": "node scripts/init-db.mjs",
|
||||||
"reset-admin-password": "node scripts/reset-admin-password.mjs",
|
"reset-admin-password": "node scripts/reset-admin-password.mjs",
|
||||||
"lint": "next lint",
|
"lint": "eslint .",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"icons": "node scripts/generate-icons.js",
|
"icons": "node scripts/generate-icons.js",
|
||||||
"postinstall": "prisma generate"
|
"postinstall": "prisma generate"
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ const OFFLINE_PAGE = "/offline.html";
|
|||||||
const PRECACHE_ASSETS = [OFFLINE_PAGE, "/manifest.json"];
|
const PRECACHE_ASSETS = [OFFLINE_PAGE, "/manifest.json"];
|
||||||
|
|
||||||
// Cache size limits
|
// Cache size limits
|
||||||
const IMAGES_CACHE_MAX_SIZE = 100 * 1024 * 1024; // 100MB
|
|
||||||
const IMAGES_CACHE_MAX_ENTRIES = 500;
|
const IMAGES_CACHE_MAX_ENTRIES = 500;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -45,11 +44,6 @@ function isBookPageRequest(url) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isBooksManualCache(url) {
|
|
||||||
// Check if this is a request that should be handled by the books manual cache
|
|
||||||
return url.includes("/api/komga/images/books/") && url.includes("/pages");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Client Communication
|
// Client Communication
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -270,7 +264,7 @@ self.addEventListener("install", (event) => {
|
|||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log("[SW] Precached assets");
|
console.log("[SW] Precached assets");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error("[SW] Precache failed:", error);
|
console.error("[SW] Precache failed:", error);
|
||||||
}
|
}
|
||||||
await self.skipWaiting();
|
await self.skipWaiting();
|
||||||
|
|||||||
@@ -1,10 +1,36 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { AdminService } from "@/lib/services/admin.service";
|
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 { AppError } from "@/utils/errors";
|
||||||
import { AuthServerService } from "@/lib/services/auth-server.service";
|
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
|
* Met à jour les rôles d'un utilisateur
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { AuthServerService } from "@/lib/services/auth-server.service";
|
import { AuthServerService } from "@/lib/services/auth-server.service";
|
||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
|
||||||
import { AppError } from "@/utils/errors";
|
import { AppError } from "@/utils/errors";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
36
src/app/actions/books.ts
Normal file
36
src/app/actions/books.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { BookService } from "@/lib/services/book.service";
|
||||||
|
import { AppError } from "@/utils/errors";
|
||||||
|
import type { KomgaBook } from "@/types/komga";
|
||||||
|
|
||||||
|
interface BookDataResult {
|
||||||
|
success: boolean;
|
||||||
|
data?: {
|
||||||
|
book: KomgaBook;
|
||||||
|
pages: number[];
|
||||||
|
nextBook: KomgaBook | null;
|
||||||
|
};
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBookData(bookId: string): Promise<BookDataResult> {
|
||||||
|
try {
|
||||||
|
const data = await BookService.getBook(bookId);
|
||||||
|
const nextBook = await BookService.getNextBook(bookId, data.book.seriesId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
nextBook,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AppError) {
|
||||||
|
return { success: false, message: error.code };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, message: "BOOK_DATA_FETCH_ERROR" };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { ConfigDBService } from "@/lib/services/config-db.service";
|
import { ConfigDBService } from "@/lib/services/config-db.service";
|
||||||
import { TestService } from "@/lib/services/test.service";
|
import { TestService } from "@/lib/services/test.service";
|
||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
|
||||||
import { AppError } from "@/utils/errors";
|
import { AppError } from "@/utils/errors";
|
||||||
import type { KomgaConfig, KomgaConfigData, KomgaLibrary } from "@/types/komga";
|
import type { KomgaConfig, KomgaConfigData, KomgaLibrary } from "@/types/komga";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { FavoriteService } from "@/lib/services/favorite.service";
|
import { FavoriteService } from "@/lib/services/favorite.service";
|
||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
|
||||||
import { AppError } from "@/utils/errors";
|
import { AppError } from "@/utils/errors";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { LibraryService } from "@/lib/services/library.service";
|
import { LibraryService } from "@/lib/services/library.service";
|
||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
import { BookService } from "@/lib/services/book.service";
|
||||||
import { AppError } from "@/utils/errors";
|
import { AppError } from "@/utils/errors";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,3 +26,25 @@ export async function scanLibrary(
|
|||||||
return { success: false, message: "Erreur lors du scan" };
|
return { success: false, message: "Erreur lors du scan" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne un livre aléatoire depuis les bibliothèques sélectionnées
|
||||||
|
*/
|
||||||
|
export async function getRandomBookFromLibraries(
|
||||||
|
libraryIds: string[]
|
||||||
|
): Promise<{ success: boolean; bookId?: string; message?: string }> {
|
||||||
|
try {
|
||||||
|
if (!libraryIds.length) {
|
||||||
|
return { success: false, message: "Au moins une bibliothèque doit être sélectionnée" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookId = await BookService.getRandomBookFromLibraries(libraryIds);
|
||||||
|
return { success: true, bookId };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AppError) {
|
||||||
|
return { success: false, message: error.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, message: "Erreur lors de la récupération d'un livre aléatoire" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { UserService } from "@/lib/services/user.service";
|
import { UserService } from "@/lib/services/user.service";
|
||||||
import { AuthServerService } from "@/lib/services/auth-server.service";
|
import { AuthServerService } from "@/lib/services/auth-server.service";
|
||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
|
||||||
import { AppError } from "@/utils/errors";
|
import { AppError } from "@/utils/errors";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { PreferencesService } from "@/lib/services/preferences.service";
|
import { PreferencesService } from "@/lib/services/preferences.service";
|
||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
|
||||||
import { AppError } from "@/utils/errors";
|
import { AppError } from "@/utils/errors";
|
||||||
import type { UserPreferences } from "@/types/preferences";
|
import type { UserPreferences } from "@/types/preferences";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { revalidateTag } from "next/cache";
|
import { revalidateTag } from "next/cache";
|
||||||
import { BookService } from "@/lib/services/book.service";
|
import { BookService } from "@/lib/services/book.service";
|
||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
|
||||||
import { AppError } from "@/utils/errors";
|
import { AppError } from "@/utils/errors";
|
||||||
|
|
||||||
const HOME_CACHE_TAG = "home-data";
|
const HOME_CACHE_TAG = "home-data";
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
import { BookService } from "@/lib/services/book.service";
|
|
||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
|
||||||
import { getErrorMessage } from "@/utils/errors";
|
|
||||||
import { AppError } from "@/utils/errors";
|
|
||||||
import type { KomgaBookWithPages } from "@/types/komga";
|
|
||||||
import type { NextRequest } from "next/server";
|
|
||||||
import logger from "@/lib/logger";
|
|
||||||
|
|
||||||
// Cache handled in service via fetchFromApi options
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ bookId: string }> }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const bookId: string = (await params).bookId;
|
|
||||||
|
|
||||||
const data: KomgaBookWithPages = await BookService.getBook(bookId);
|
|
||||||
const nextBook = await BookService.getNextBook(bookId, data.book.seriesId);
|
|
||||||
|
|
||||||
return NextResponse.json({ ...data, nextBook });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, "API Books - Erreur:");
|
|
||||||
if (error instanceof AppError) {
|
|
||||||
const isNotFound =
|
|
||||||
error.code === ERROR_CODES.BOOK.NOT_FOUND ||
|
|
||||||
(error.code === ERROR_CODES.KOMGA.HTTP_ERROR && (error as any).params?.status === 404);
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: error.code,
|
|
||||||
name: "Book fetch error",
|
|
||||||
message: getErrorMessage(error.code),
|
|
||||||
} as AppError,
|
|
||||||
},
|
|
||||||
{ status: isNotFound ? 404 : 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: ERROR_CODES.BOOK.NOT_FOUND,
|
|
||||||
name: "Book fetch error",
|
|
||||||
message: getErrorMessage(ERROR_CODES.BOOK.NOT_FOUND),
|
|
||||||
} as AppError,
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
import { HomeService } from "@/lib/services/home.service";
|
|
||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
|
||||||
import { AppError } from "@/utils/errors";
|
|
||||||
import { getErrorMessage } from "@/utils/errors";
|
|
||||||
import logger from "@/lib/logger";
|
|
||||||
|
|
||||||
// Cache handled in service via fetchFromApi options
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
const data = await HomeService.getHomeData();
|
|
||||||
return NextResponse.json(data);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, "API Home - Erreur:");
|
|
||||||
if (error instanceof AppError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: error.code,
|
|
||||||
name: "Home data fetch error",
|
|
||||||
message: getErrorMessage(error.code),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: error.code === ERROR_CODES.KOMGA.MISSING_CONFIG ? 404 : 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: ERROR_CODES.KOMGA.SERVER_UNREACHABLE,
|
|
||||||
name: "Home data fetch error",
|
|
||||||
message: getErrorMessage(ERROR_CODES.KOMGA.SERVER_UNREACHABLE),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,7 +26,7 @@ export async function GET(
|
|||||||
|
|
||||||
if (httpStatus === 404) {
|
if (httpStatus === 404) {
|
||||||
const { bookId, pageNumber } = await params;
|
const { bookId, pageNumber } = await params;
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
logger.info(`📷 Page ${pageNumber} not found for book: ${bookId}`);
|
logger.info(`📷 Page ${pageNumber} not found for book: ${bookId}`);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export async function GET(
|
|||||||
if (httpStatus === 404) {
|
if (httpStatus === 404) {
|
||||||
const { bookId, pageNumber: pageNumberParam } = await params;
|
const { bookId, pageNumber: pageNumberParam } = await params;
|
||||||
const pageNumber: number = parseInt(pageNumberParam);
|
const pageNumber: number = parseInt(pageNumberParam);
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
logger.info(`📷 Page ${pageNumber} thumbnail not found for book: ${bookId}`);
|
logger.info(`📷 Page ${pageNumber} thumbnail not found for book: ${bookId}`);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export async function GET(
|
|||||||
|
|
||||||
if (httpStatus === 404) {
|
if (httpStatus === 404) {
|
||||||
const bookId: string = (await params).bookId;
|
const bookId: string = (await params).bookId;
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
logger.info(`📷 Thumbnail not found for book: ${bookId}`);
|
logger.info(`📷 Thumbnail not found for book: ${bookId}`);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export async function GET(
|
|||||||
|
|
||||||
if (httpStatus === 404) {
|
if (httpStatus === 404) {
|
||||||
const seriesId: string = (await params).seriesId;
|
const seriesId: string = (await params).seriesId;
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
logger.info(`📷 First page image not found for series: ${seriesId}`);
|
logger.info(`📷 First page image not found for series: ${seriesId}`);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,57 +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 { getErrorMessage } from "@/utils/errors";
|
|
||||||
import type { NextRequest } from "next/server";
|
|
||||||
import logger from "@/lib/logger";
|
|
||||||
|
|
||||||
// Cache handled in service via fetchFromApi options
|
|
||||||
|
|
||||||
const DEFAULT_PAGE_SIZE = 20;
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ libraryId: string }> }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const libraryId: string = (await params).libraryId;
|
|
||||||
const searchParams = request.nextUrl.searchParams;
|
|
||||||
|
|
||||||
const page = parseInt(searchParams.get("page") || "0");
|
|
||||||
const size = parseInt(searchParams.get("size") || String(DEFAULT_PAGE_SIZE));
|
|
||||||
const unreadOnly = searchParams.get("unread") === "true";
|
|
||||||
const search = searchParams.get("search") || undefined;
|
|
||||||
|
|
||||||
const [series, library] = await Promise.all([
|
|
||||||
LibraryService.getLibrarySeries(libraryId, page, size, unreadOnly, search),
|
|
||||||
LibraryService.getLibrary(libraryId),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return NextResponse.json({ series, library });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, "API Library Series - Erreur:");
|
|
||||||
if (error instanceof AppError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: error.code,
|
|
||||||
name: "Library series fetch error",
|
|
||||||
message: getErrorMessage(error.code),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: ERROR_CODES.SERIES.FETCH_ERROR,
|
|
||||||
name: "Library series fetch error",
|
|
||||||
message: getErrorMessage(ERROR_CODES.SERIES.FETCH_ERROR),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
import type { NextRequest } from "next/server";
|
|
||||||
import { BookService } from "@/lib/services/book.service";
|
|
||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
|
||||||
import { AppError } from "@/utils/errors";
|
|
||||||
import { getErrorMessage } from "@/utils/errors";
|
|
||||||
import logger from "@/lib/logger";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { searchParams } = new URL(request.url);
|
|
||||||
const libraryIds = searchParams.get("libraryIds")?.split(",") || [];
|
|
||||||
|
|
||||||
if (libraryIds.length === 0) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: ERROR_CODES.LIBRARY.FETCH_ERROR,
|
|
||||||
message: "Au moins une bibliothèque doit être sélectionnée",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bookId = await BookService.getRandomBookFromLibraries(libraryIds);
|
|
||||||
return NextResponse.json({ bookId });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, "Erreur lors de la récupération d'un livre aléatoire");
|
|
||||||
if (error instanceof AppError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: error.code,
|
|
||||||
message: getErrorMessage(error.code),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: ERROR_CODES.SERIES.FETCH_ERROR,
|
|
||||||
message: getErrorMessage(ERROR_CODES.SERIES.FETCH_ERROR),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
import { SeriesService } from "@/lib/services/series.service";
|
|
||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
|
||||||
import { AppError } from "@/utils/errors";
|
|
||||||
import { getErrorMessage } from "@/utils/errors";
|
|
||||||
import type { NextRequest } from "next/server";
|
|
||||||
import logger from "@/lib/logger";
|
|
||||||
|
|
||||||
// Cache handled in service via fetchFromApi options
|
|
||||||
|
|
||||||
const DEFAULT_PAGE_SIZE = 20;
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ seriesId: string }> }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const seriesId: string = (await params).seriesId;
|
|
||||||
const searchParams = request.nextUrl.searchParams;
|
|
||||||
|
|
||||||
const page = parseInt(searchParams.get("page") || "0");
|
|
||||||
const size = parseInt(searchParams.get("size") || String(DEFAULT_PAGE_SIZE));
|
|
||||||
const unreadOnly = searchParams.get("unread") === "true";
|
|
||||||
|
|
||||||
const [books, series] = await Promise.all([
|
|
||||||
SeriesService.getSeriesBooks(seriesId, page, size, unreadOnly),
|
|
||||||
SeriesService.getSeries(seriesId),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return NextResponse.json({ books, series });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, "API Series Books - Erreur:");
|
|
||||||
if (error instanceof AppError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: error.code,
|
|
||||||
name: "Series books fetch error",
|
|
||||||
message: getErrorMessage(error.code),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: ERROR_CODES.BOOK.PAGES_FETCH_ERROR,
|
|
||||||
name: "Series books fetch error",
|
|
||||||
message: getErrorMessage(ERROR_CODES.BOOK.PAGES_FETCH_ERROR),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
import { SeriesService } from "@/lib/services/series.service";
|
|
||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
|
||||||
import { AppError } from "@/utils/errors";
|
|
||||||
import type { KomgaSeries } from "@/types/komga";
|
|
||||||
import { getErrorMessage } from "@/utils/errors";
|
|
||||||
import type { NextRequest } from "next/server";
|
|
||||||
import logger from "@/lib/logger";
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ seriesId: string }> }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const seriesId: string = (await params).seriesId;
|
|
||||||
|
|
||||||
const series: KomgaSeries = await SeriesService.getSeries(seriesId);
|
|
||||||
return NextResponse.json(series);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, "API Series - Erreur:");
|
|
||||||
if (error instanceof AppError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: error.code,
|
|
||||||
name: "Series fetch error",
|
|
||||||
message: getErrorMessage(error.code),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: ERROR_CODES.SERIES.FETCH_ERROR,
|
|
||||||
name: "Series fetch error",
|
|
||||||
message: getErrorMessage(ERROR_CODES.SERIES.FETCH_ERROR),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
import { UserService } from "@/lib/services/user.service";
|
|
||||||
import { AppError } from "@/utils/errors";
|
|
||||||
import logger from "@/lib/logger";
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
const [profile, stats] = await Promise.all([
|
|
||||||
UserService.getUserProfile(),
|
|
||||||
UserService.getUserStats(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return NextResponse.json({ ...profile, stats });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, "Erreur lors de la récupération du profil:");
|
|
||||||
|
|
||||||
if (error instanceof AppError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message, code: error.code },
|
|
||||||
{ status: error.code === "AUTH_UNAUTHENTICATED" ? 401 : 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Erreur lors de la récupération du profil" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,7 @@ import { AuthProvider } from "@/components/providers/AuthProvider";
|
|||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { defaultPreferences } from "@/types/preferences";
|
import { defaultPreferences } from "@/types/preferences";
|
||||||
import type { UserPreferences } from "@/types/preferences";
|
import type { UserPreferences } from "@/types/preferences";
|
||||||
|
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
||||||
import logger from "@/lib/logger";
|
import logger from "@/lib/logger";
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
@@ -73,8 +74,8 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||||||
|
|
||||||
let preferences: UserPreferences = defaultPreferences;
|
let preferences: UserPreferences = defaultPreferences;
|
||||||
let userIsAdmin = false;
|
let userIsAdmin = false;
|
||||||
let libraries: any[] = [];
|
let libraries: KomgaLibrary[] = [];
|
||||||
let favorites: any[] = [];
|
let favorites: KomgaSeries[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [preferencesData, isAdminCheck, librariesData, favoritesData] = await Promise.allSettled([
|
const [preferencesData, isAdminCheck, librariesData, favoritesData] = await Promise.allSettled([
|
||||||
|
|||||||
@@ -5,51 +5,18 @@ import { useRouter } from "next/navigation";
|
|||||||
import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator";
|
import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator";
|
||||||
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
|
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
|
||||||
import { RefreshProvider } from "@/contexts/RefreshContext";
|
import { RefreshProvider } from "@/contexts/RefreshContext";
|
||||||
import type { UserPreferences } from "@/types/preferences";
|
|
||||||
|
|
||||||
interface LibraryClientWrapperProps {
|
interface LibraryClientWrapperProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
libraryId: string;
|
|
||||||
currentPage: number;
|
|
||||||
unreadOnly: boolean;
|
|
||||||
search?: string;
|
|
||||||
pageSize: number;
|
|
||||||
preferences: UserPreferences;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LibraryClientWrapper({
|
export function LibraryClientWrapper({ children }: LibraryClientWrapperProps) {
|
||||||
children,
|
|
||||||
libraryId,
|
|
||||||
currentPage,
|
|
||||||
unreadOnly,
|
|
||||||
search,
|
|
||||||
pageSize,
|
|
||||||
}: LibraryClientWrapperProps) {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
try {
|
try {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
|
|
||||||
// Fetch fresh data from network with cache bypass
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
page: String(currentPage),
|
|
||||||
size: String(pageSize),
|
|
||||||
...(unreadOnly && { unreadOnly: "true" }),
|
|
||||||
...(search && { search }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(`/api/komga/libraries/${libraryId}/series?${params}`, {
|
|
||||||
cache: "no-store",
|
|
||||||
headers: { "Cache-Control": "no-cache" },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to refresh library");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger Next.js revalidation to update the UI
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -43,14 +43,7 @@ export default async function LibraryPage({ params, searchParams }: PageProps) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LibraryClientWrapper
|
<LibraryClientWrapper>
|
||||||
libraryId={libraryId}
|
|
||||||
currentPage={currentPage}
|
|
||||||
unreadOnly={unreadOnly}
|
|
||||||
search={search}
|
|
||||||
pageSize={effectivePageSize}
|
|
||||||
preferences={preferences}
|
|
||||||
>
|
|
||||||
<LibraryContent
|
<LibraryContent
|
||||||
library={library}
|
library={library}
|
||||||
series={series}
|
series={series}
|
||||||
|
|||||||
@@ -5,23 +5,13 @@ import { useRouter } from "next/navigation";
|
|||||||
import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator";
|
import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator";
|
||||||
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
|
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
|
||||||
import { RefreshProvider } from "@/contexts/RefreshContext";
|
import { RefreshProvider } from "@/contexts/RefreshContext";
|
||||||
import type { UserPreferences } from "@/types/preferences";
|
|
||||||
|
|
||||||
interface SeriesClientWrapperProps {
|
interface SeriesClientWrapperProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
seriesId: string;
|
|
||||||
currentPage: number;
|
|
||||||
unreadOnly: boolean;
|
|
||||||
pageSize: number;
|
|
||||||
preferences: UserPreferences;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SeriesClientWrapper({
|
export function SeriesClientWrapper({
|
||||||
children,
|
children,
|
||||||
seriesId,
|
|
||||||
currentPage,
|
|
||||||
unreadOnly,
|
|
||||||
pageSize,
|
|
||||||
}: SeriesClientWrapperProps) {
|
}: SeriesClientWrapperProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
@@ -29,24 +19,6 @@ export function SeriesClientWrapper({
|
|||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
try {
|
try {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
|
|
||||||
// Fetch fresh data from network with cache bypass
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
page: String(currentPage),
|
|
||||||
size: String(pageSize),
|
|
||||||
...(unreadOnly && { unreadOnly: "true" }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(`/api/komga/series/${seriesId}/books?${params}`, {
|
|
||||||
cache: "no-store",
|
|
||||||
headers: { "Cache-Control": "no-cache" },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to refresh series");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger Next.js revalidation to update the UI
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface SeriesContentProps {
|
|||||||
preferences: UserPreferences;
|
preferences: UserPreferences;
|
||||||
unreadOnly: boolean;
|
unreadOnly: boolean;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
|
initialIsFavorite: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SeriesContent({
|
export function SeriesContent({
|
||||||
@@ -23,6 +24,7 @@ export function SeriesContent({
|
|||||||
currentPage,
|
currentPage,
|
||||||
preferences,
|
preferences,
|
||||||
unreadOnly,
|
unreadOnly,
|
||||||
|
initialIsFavorite,
|
||||||
}: SeriesContentProps) {
|
}: SeriesContentProps) {
|
||||||
const { refreshSeries } = useRefresh();
|
const { refreshSeries } = useRefresh();
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ export function SeriesContent({
|
|||||||
<SeriesHeader
|
<SeriesHeader
|
||||||
series={series}
|
series={series}
|
||||||
refreshSeries={refreshSeries || (async () => ({ success: false }))}
|
refreshSeries={refreshSeries || (async () => ({ success: false }))}
|
||||||
|
initialIsFavorite={initialIsFavorite}
|
||||||
/>
|
/>
|
||||||
<Container>
|
<Container>
|
||||||
<PaginatedBookGrid
|
<PaginatedBookGrid
|
||||||
@@ -45,4 +48,3 @@ export function SeriesContent({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PreferencesService } from "@/lib/services/preferences.service";
|
import { PreferencesService } from "@/lib/services/preferences.service";
|
||||||
import { SeriesService } from "@/lib/services/series.service";
|
import { SeriesService } from "@/lib/services/series.service";
|
||||||
|
import { FavoriteService } from "@/lib/services/favorite.service";
|
||||||
import { SeriesClientWrapper } from "./SeriesClientWrapper";
|
import { SeriesClientWrapper } from "./SeriesClientWrapper";
|
||||||
import { SeriesContent } from "./SeriesContent";
|
import { SeriesContent } from "./SeriesContent";
|
||||||
import { ErrorMessage } from "@/components/ui/ErrorMessage";
|
import { ErrorMessage } from "@/components/ui/ErrorMessage";
|
||||||
@@ -28,19 +29,14 @@ export default async function SeriesPage({ params, searchParams }: PageProps) {
|
|||||||
const effectivePageSize = size ? parseInt(size) : preferences.displayMode?.itemsPerPage || DEFAULT_PAGE_SIZE;
|
const effectivePageSize = size ? parseInt(size) : preferences.displayMode?.itemsPerPage || DEFAULT_PAGE_SIZE;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [books, series] = await Promise.all([
|
const [books, series, isFavorite] = await Promise.all([
|
||||||
SeriesService.getSeriesBooks(seriesId, currentPage - 1, effectivePageSize, unreadOnly),
|
SeriesService.getSeriesBooks(seriesId, currentPage - 1, effectivePageSize, unreadOnly),
|
||||||
SeriesService.getSeries(seriesId),
|
SeriesService.getSeries(seriesId),
|
||||||
|
FavoriteService.isFavorite(seriesId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SeriesClientWrapper
|
<SeriesClientWrapper>
|
||||||
seriesId={seriesId}
|
|
||||||
currentPage={currentPage}
|
|
||||||
unreadOnly={unreadOnly}
|
|
||||||
pageSize={effectivePageSize}
|
|
||||||
preferences={preferences}
|
|
||||||
>
|
|
||||||
<SeriesContent
|
<SeriesContent
|
||||||
series={series}
|
series={series}
|
||||||
books={books}
|
books={books}
|
||||||
@@ -48,6 +44,7 @@ export default async function SeriesPage({ params, searchParams }: PageProps) {
|
|||||||
preferences={preferences}
|
preferences={preferences}
|
||||||
unreadOnly={unreadOnly}
|
unreadOnly={unreadOnly}
|
||||||
pageSize={effectivePageSize}
|
pageSize={effectivePageSize}
|
||||||
|
initialIsFavorite={isFavorite}
|
||||||
/>
|
/>
|
||||||
</SeriesClientWrapper>
|
</SeriesClientWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { ConfigDBService } from "@/lib/services/config-db.service";
|
import { ConfigDBService } from "@/lib/services/config-db.service";
|
||||||
|
import { LibraryService } from "@/lib/services/library.service";
|
||||||
import { ClientSettings } from "@/components/settings/ClientSettings";
|
import { ClientSettings } from "@/components/settings/ClientSettings";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import type { KomgaConfig } from "@/types/komga";
|
import type { KomgaConfig, KomgaLibrary } from "@/types/komga";
|
||||||
import logger from "@/lib/logger";
|
import logger from "@/lib/logger";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -13,6 +14,7 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default async function SettingsPage() {
|
export default async function SettingsPage() {
|
||||||
let config: KomgaConfig | null = null;
|
let config: KomgaConfig | null = null;
|
||||||
|
let libraries: KomgaLibrary[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Récupérer la configuration Komga
|
// Récupérer la configuration Komga
|
||||||
@@ -26,10 +28,12 @@ export default async function SettingsPage() {
|
|||||||
password: null,
|
password: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
libraries = await LibraryService.getLibraries();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, "Erreur lors de la récupération de la configuration:");
|
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
|
// On ne fait rien si la config n'existe pas, on laissera le composant client gérer l'état initial
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ClientSettings initialConfig={config} />;
|
return <ClientSettings initialConfig={config} initialLibraries={libraries} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback, useEffect } from "react";
|
||||||
import type { AdminUserData } from "@/lib/services/admin.service";
|
import type { AdminUserData } from "@/lib/services/admin.service";
|
||||||
import { StatsCards } from "./StatsCards";
|
import { StatsCards } from "./StatsCards";
|
||||||
import { UsersTable } from "./UsersTable";
|
import { UsersTable } from "./UsersTable";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RefreshCw } from "lucide-react";
|
import { RefreshCw } from "lucide-react";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { getAdminDashboardData, type AdminStatsData } from "@/app/actions/admin";
|
||||||
|
|
||||||
interface AdminContentProps {
|
interface AdminContentProps {
|
||||||
initialUsers: AdminUserData[];
|
initialUsers: AdminUserData[];
|
||||||
initialStats: {
|
initialStats: AdminStatsData;
|
||||||
totalUsers: number;
|
|
||||||
totalAdmins: number;
|
|
||||||
usersWithKomga: number;
|
|
||||||
usersWithPreferences: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AdminContent({ initialUsers, initialStats }: AdminContentProps) {
|
export function AdminContent({ initialUsers, initialStats }: AdminContentProps) {
|
||||||
@@ -24,22 +20,25 @@ export function AdminContent({ initialUsers, initialStats }: AdminContentProps)
|
|||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setUsers(initialUsers);
|
||||||
|
}, [initialUsers]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStats(initialStats);
|
||||||
|
}, [initialStats]);
|
||||||
|
|
||||||
const refreshData = useCallback(async () => {
|
const refreshData = useCallback(async () => {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
try {
|
try {
|
||||||
const [usersResponse, statsResponse] = await Promise.all([
|
const result = await getAdminDashboardData();
|
||||||
fetch("/api/admin/users"),
|
|
||||||
fetch("/api/admin/stats"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!usersResponse.ok || !statsResponse.ok) {
|
if (!result.success || !result.users || !result.stats) {
|
||||||
throw new Error("Erreur lors du rafraîchissement");
|
throw new Error("Erreur lors du rafraîchissement");
|
||||||
}
|
}
|
||||||
|
|
||||||
const [newUsers, newStats] = await Promise.all([usersResponse.json(), statsResponse.json()]);
|
setUsers(result.users);
|
||||||
|
setStats(result.stats);
|
||||||
setUsers(newUsers);
|
|
||||||
setStats(newStats);
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Données rafraîchies",
|
title: "Données rafraîchies",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { BookOfflineButton } from "@/components/ui/book-offline-button";
|
|||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
import logger from "@/lib/logger";
|
import logger from "@/lib/logger";
|
||||||
import { Container } from "@/components/ui/container";
|
import { Container } from "@/components/ui/container";
|
||||||
|
import { getBookData } from "@/app/actions/books";
|
||||||
|
|
||||||
type BookStatus = "idle" | "downloading" | "available" | "error";
|
type BookStatus = "idle" | "downloading" | "available" | "error";
|
||||||
|
|
||||||
@@ -45,17 +46,18 @@ export function DownloadManager() {
|
|||||||
const key = localStorage.key(i);
|
const key = localStorage.key(i);
|
||||||
if (key?.startsWith("book-status-")) {
|
if (key?.startsWith("book-status-")) {
|
||||||
const bookId = key.replace("book-status-", "");
|
const bookId = key.replace("book-status-", "");
|
||||||
const status = JSON.parse(localStorage.getItem(key) || "");
|
const status = JSON.parse(localStorage.getItem(key) || "");
|
||||||
if (status.status !== "idle") {
|
if (status.status !== "idle") {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/komga/books/${bookId}`);
|
const result = await getBookData(bookId);
|
||||||
if (!response.ok) throw new Error("Livre non trouvé");
|
if (!result.success || !result.data) {
|
||||||
const bookData = await response.json();
|
throw new Error("Livre non trouvé");
|
||||||
books.push({
|
}
|
||||||
book: bookData.book,
|
books.push({
|
||||||
status,
|
book: result.data.book,
|
||||||
});
|
status,
|
||||||
} catch (error) {
|
});
|
||||||
|
} catch (error) {
|
||||||
logger.error({ err: error }, `Erreur lors de la récupération du livre ${bookId}:`);
|
logger.error({ err: error }, `Erreur lors de la récupération du livre ${bookId}:`);
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { RefreshButton } from "@/components/library/RefreshButton";
|
|||||||
import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator";
|
import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator";
|
||||||
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
|
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
|
||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
import logger from "@/lib/logger";
|
|
||||||
|
|
||||||
interface HomeClientWrapperProps {
|
interface HomeClientWrapperProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -20,22 +19,10 @@ export function HomeClientWrapper({ children }: HomeClientWrapperProps) {
|
|||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
try {
|
try {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
|
// Re-fetch server-side data
|
||||||
// Fetch fresh data from network with cache bypass
|
|
||||||
const response = await fetch("/api/komga/home", {
|
|
||||||
cache: "no-store",
|
|
||||||
headers: { "Cache-Control": "no-cache" },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to refresh home");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger Next.js revalidation to update the UI
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
logger.error({ err: error }, "Erreur lors du rafraîchissement:");
|
|
||||||
return { success: false, error: "Erreur lors du rafraîchissement de la page d'accueil" };
|
return { success: false, error: "Erreur lors du rafraîchissement de la page d'accueil" };
|
||||||
} finally {
|
} finally {
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { usePreferences } from "@/contexts/PreferencesContext";
|
|||||||
import { ServiceWorkerProvider } from "@/contexts/ServiceWorkerContext";
|
import { ServiceWorkerProvider } from "@/contexts/ServiceWorkerContext";
|
||||||
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
||||||
import logger from "@/lib/logger";
|
import logger from "@/lib/logger";
|
||||||
|
import { getRandomBookFromLibraries } from "@/app/actions/library";
|
||||||
|
|
||||||
// Routes qui ne nécessitent pas d'authentification
|
// Routes qui ne nécessitent pas d'authentification
|
||||||
const publicRoutes = ["/login", "/register"];
|
const publicRoutes = ["/login", "/register"];
|
||||||
@@ -51,10 +52,11 @@ export default function ClientLayout({
|
|||||||
const fetchRandomBook = useCallback(async () => {
|
const fetchRandomBook = useCallback(async () => {
|
||||||
if (backgroundType === "komga-random" && libraryIdsString) {
|
if (backgroundType === "komga-random" && libraryIdsString) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/komga/random-book?libraryIds=${libraryIdsString}`);
|
const libraryIds = libraryIdsString.split(",").filter(Boolean);
|
||||||
if (response.ok) {
|
const result = await getRandomBookFromLibraries(libraryIds);
|
||||||
const data = await response.json();
|
|
||||||
setRandomBookId(data.bookId);
|
if (result.success && result.bookId) {
|
||||||
|
setRandomBookId(result.bookId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, "Erreur lors de la récupération d'un book aléatoire:");
|
logger.error({ err: error }, "Erreur lors de la récupération d'un book aléatoire:");
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ import { cn } from "@/lib/utils";
|
|||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
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 { useToast } from "@/components/ui/use-toast";
|
||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
import { NavButton } from "@/components/ui/nav-button";
|
import { NavButton } from "@/components/ui/nav-button";
|
||||||
@@ -49,73 +46,42 @@ export function Sidebar({
|
|||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const refreshFavorites = useCallback(async () => {
|
useEffect(() => {
|
||||||
try {
|
setLibraries(initialLibraries || []);
|
||||||
const favoritesResponse = await fetch("/api/komga/favorites");
|
}, [initialLibraries]);
|
||||||
if (!favoritesResponse.ok) {
|
|
||||||
throw new AppError(ERROR_CODES.FAVORITE.FETCH_ERROR);
|
|
||||||
}
|
|
||||||
const favoriteIds = await favoritesResponse.json();
|
|
||||||
|
|
||||||
if (favoriteIds.length === 0) {
|
useEffect(() => {
|
||||||
setFavorites([]);
|
setFavorites(initialFavorites || []);
|
||||||
return;
|
}, [initialFavorites]);
|
||||||
}
|
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
// Mettre à jour les favoris quand ils changent (mise à jour optimiste)
|
// Mettre à jour les favoris quand ils changent (mise à jour optimiste)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFavoritesChange = async (event: Event) => {
|
const handleFavoritesChange = (event: Event) => {
|
||||||
const customEvent = event as CustomEvent<{ seriesId: string; action: "add" | "remove" }>;
|
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
|
// Si on a les détails de l'action, faire une mise à jour optimiste locale
|
||||||
if (customEvent.detail?.seriesId) {
|
if (customEvent.detail?.seriesId) {
|
||||||
const { seriesId, action } = customEvent.detail;
|
const { seriesId, action, series } = customEvent.detail;
|
||||||
|
|
||||||
if (action === "add") {
|
if (action === "add" && series) {
|
||||||
// Fetch les détails de la série ajoutée et l'ajouter au state
|
setFavorites((prev) => {
|
||||||
try {
|
if (prev.some((s) => s.id === series.id)) {
|
||||||
const response = await fetch(`/api/komga/series/${seriesId}`);
|
return prev;
|
||||||
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];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, "Erreur lors de l'ajout optimiste du favori:");
|
return [...prev, series];
|
||||||
}
|
});
|
||||||
} else if (action === "remove") {
|
} else if (action === "remove") {
|
||||||
// Retirer la série du state directement
|
|
||||||
setFavorites((prev) => prev.filter((s) => s.id !== seriesId));
|
setFavorites((prev) => prev.filter((s) => s.id !== seriesId));
|
||||||
|
} else {
|
||||||
|
router.refresh();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: refetch complet si pas de détails (ex: événement externe)
|
router.refresh();
|
||||||
refreshFavorites();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -124,7 +90,7 @@ export function Sidebar({
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("favoritesChanged", handleFavoritesChange);
|
window.removeEventListener("favoritesChanged", handleFavoritesChange);
|
||||||
};
|
};
|
||||||
}, [refreshFavorites]);
|
}, [router]);
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ interface SeriesGridProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Utility function to get reading status info
|
// Utility function to get reading status info
|
||||||
const getReadingStatusInfo = (series: KomgaSeries, t: (key: string, options?: any) => string) => {
|
const getReadingStatusInfo = (
|
||||||
|
series: KomgaSeries,
|
||||||
|
t: (key: string, options?: { [key: string]: string | number }) => string
|
||||||
|
) => {
|
||||||
if (series.booksCount === 0) {
|
if (series.booksCount === 0) {
|
||||||
return {
|
return {
|
||||||
label: t("series.status.noBooks"),
|
label: t("series.status.noBooks"),
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ interface SeriesListItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Utility function to get reading status info
|
// Utility function to get reading status info
|
||||||
const getReadingStatusInfo = (series: KomgaSeries, t: (key: string, options?: any) => string) => {
|
const getReadingStatusInfo = (
|
||||||
|
series: KomgaSeries,
|
||||||
|
t: (key: string, options?: { [key: string]: string | number }) => string
|
||||||
|
) => {
|
||||||
if (series.booksCount === 0) {
|
if (series.booksCount === 0) {
|
||||||
return {
|
return {
|
||||||
label: t("series.status.noBooks"),
|
label: t("series.status.noBooks"),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ErrorMessage } from "@/components/ui/ErrorMessage";
|
|||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||||
import type { KomgaBook } from "@/types/komga";
|
import type { KomgaBook } from "@/types/komga";
|
||||||
import logger from "@/lib/logger";
|
import logger from "@/lib/logger";
|
||||||
|
import { getBookData } from "@/app/actions/books";
|
||||||
|
|
||||||
interface ClientBookPageProps {
|
interface ClientBookPageProps {
|
||||||
bookId: string;
|
bookId: string;
|
||||||
@@ -48,15 +49,12 @@ export function ClientBookPage({ bookId, initialData, initialError }: ClientBook
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const response = await fetch(`/api/komga/books/${bookId}`);
|
const result = await getBookData(bookId);
|
||||||
|
if (!result.success || !result.data) {
|
||||||
if (!response.ok) {
|
throw new Error(result.message || ERROR_CODES.BOOK.PAGES_FETCH_ERROR);
|
||||||
const errorData = await response.json();
|
|
||||||
throw new Error(errorData.error?.code || ERROR_CODES.BOOK.PAGES_FETCH_ERROR);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bookData = await response.json();
|
setData(result.data);
|
||||||
setData(bookData);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ err }, "Error fetching book");
|
logger.error({ err }, "Error fetching book");
|
||||||
setError(err instanceof Error ? err.message : ERROR_CODES.BOOK.PAGES_FETCH_ERROR);
|
setError(err instanceof Error ? err.message : ERROR_CODES.BOOK.PAGES_FETCH_ERROR);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useCallback, useRef } from "react";
|
import { useEffect, useState, useCallback, useRef } from "react";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useReadingDirection } from "./useReadingDirection";
|
|||||||
interface UseTouchNavigationProps {
|
interface UseTouchNavigationProps {
|
||||||
onPreviousPage: () => void;
|
onPreviousPage: () => void;
|
||||||
onNextPage: () => void;
|
onNextPage: () => void;
|
||||||
pswpRef: React.MutableRefObject<any>;
|
pswpRef: React.MutableRefObject<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTouchNavigation({
|
export function useTouchNavigation({
|
||||||
|
|||||||
@@ -18,37 +18,17 @@ import { addToFavorites, removeFromFavorites } from "@/app/actions/favorites";
|
|||||||
interface SeriesHeaderProps {
|
interface SeriesHeaderProps {
|
||||||
series: KomgaSeries;
|
series: KomgaSeries;
|
||||||
refreshSeries: (seriesId: string) => Promise<{ success: boolean; error?: string }>;
|
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 { toast } = useToast();
|
||||||
const [isFavorite, setIsFavorite] = useState(false);
|
const [isFavorite, setIsFavorite] = useState(initialIsFavorite);
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkFavorite = async () => {
|
setIsFavorite(initialIsFavorite);
|
||||||
try {
|
}, [series.id, initialIsFavorite]);
|
||||||
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]);
|
|
||||||
|
|
||||||
const handleToggleFavorite = async () => {
|
const handleToggleFavorite = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -59,7 +39,11 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {
|
|||||||
setIsFavorite(!isFavorite);
|
setIsFavorite(!isFavorite);
|
||||||
// Dispatcher l'événement avec le seriesId pour mise à jour optimiste de la sidebar
|
// Dispatcher l'événement avec le seriesId pour mise à jour optimiste de la sidebar
|
||||||
const event = new CustomEvent("favoritesChanged", {
|
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);
|
window.dispatchEvent(event);
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -17,34 +17,28 @@ import { SliderControl } from "@/components/ui/slider-control";
|
|||||||
import type { KomgaLibrary } from "@/types/komga";
|
import type { KomgaLibrary } from "@/types/komga";
|
||||||
import logger from "@/lib/logger";
|
import logger from "@/lib/logger";
|
||||||
|
|
||||||
export function BackgroundSettings() {
|
interface BackgroundSettingsProps {
|
||||||
|
initialLibraries: KomgaLibrary[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BackgroundSettings({ initialLibraries }: BackgroundSettingsProps) {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { preferences, updatePreferences } = usePreferences();
|
const { preferences, updatePreferences } = usePreferences();
|
||||||
const [customImageUrl, setCustomImageUrl] = useState(preferences.background.imageUrl || "");
|
const [customImageUrl, setCustomImageUrl] = useState(preferences.background.imageUrl || "");
|
||||||
const [komgaConfigValid, setKomgaConfigValid] = useState(false);
|
const [komgaConfigValid, setKomgaConfigValid] = useState(false);
|
||||||
const [libraries, setLibraries] = useState<KomgaLibrary[]>([]);
|
const [libraries, setLibraries] = useState<KomgaLibrary[]>(initialLibraries || []);
|
||||||
const [selectedLibraries, setSelectedLibraries] = useState<string[]>(
|
const [selectedLibraries, setSelectedLibraries] = useState<string[]>(
|
||||||
preferences.background.komgaLibraries || []
|
preferences.background.komgaLibraries || []
|
||||||
);
|
);
|
||||||
|
|
||||||
// Vérifier la config Komga au chargement
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkKomgaConfig = async () => {
|
setLibraries(initialLibraries || []);
|
||||||
try {
|
}, [initialLibraries]);
|
||||||
const response = await fetch("/api/komga/libraries");
|
|
||||||
if (response.ok) {
|
useEffect(() => {
|
||||||
const libs = await response.json();
|
setKomgaConfigValid(libraries.length > 0);
|
||||||
setLibraries(libs);
|
}, [libraries]);
|
||||||
setKomgaConfigValid(libs.length > 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, "Erreur lors de la vérification de la config Komga:");
|
|
||||||
setKomgaConfigValid(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
checkKomgaConfig();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleBackgroundTypeChange = async (type: BackgroundType) => {
|
const handleBackgroundTypeChange = async (type: BackgroundType) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { KomgaConfig } from "@/types/komga";
|
import type { KomgaConfig } from "@/types/komga";
|
||||||
|
import type { KomgaLibrary } from "@/types/komga";
|
||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
import { DisplaySettings } from "./DisplaySettings";
|
import { DisplaySettings } from "./DisplaySettings";
|
||||||
import { KomgaSettings } from "./KomgaSettings";
|
import { KomgaSettings } from "./KomgaSettings";
|
||||||
@@ -12,9 +13,10 @@ import { Monitor, Network } from "lucide-react";
|
|||||||
|
|
||||||
interface ClientSettingsProps {
|
interface ClientSettingsProps {
|
||||||
initialConfig: KomgaConfig | null;
|
initialConfig: KomgaConfig | null;
|
||||||
|
initialLibraries: KomgaLibrary[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ClientSettings({ initialConfig }: ClientSettingsProps) {
|
export function ClientSettings({ initialConfig, initialLibraries }: ClientSettingsProps) {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -35,7 +37,7 @@ export function ClientSettings({ initialConfig }: ClientSettingsProps) {
|
|||||||
|
|
||||||
<TabsContent value="display" className="mt-6 space-y-6">
|
<TabsContent value="display" className="mt-6 space-y-6">
|
||||||
<DisplaySettings />
|
<DisplaySettings />
|
||||||
<BackgroundSettings />
|
<BackgroundSettings initialLibraries={initialLibraries} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="connection" className="mt-6 space-y-6">
|
<TabsContent value="connection" className="mt-6 space-y-6">
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ interface BeforeInstallPromptEvent extends Event {
|
|||||||
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
|
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NavigatorStandalone extends Navigator {
|
||||||
|
standalone?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const DISMISS_KEY = "pwa-install-dismissed";
|
const DISMISS_KEY = "pwa-install-dismissed";
|
||||||
const DISMISS_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 jours en millisecondes
|
const DISMISS_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 jours en millisecondes
|
||||||
|
|
||||||
@@ -24,7 +28,7 @@ export function InstallPWA() {
|
|||||||
const checkStandalone = () => {
|
const checkStandalone = () => {
|
||||||
return (
|
return (
|
||||||
window.matchMedia("(display-mode: standalone)").matches ||
|
window.matchMedia("(display-mode: standalone)").matches ||
|
||||||
(window.navigator as any).standalone ||
|
(window.navigator as NavigatorStandalone).standalone ||
|
||||||
document.referrer.includes("android-app://")
|
document.referrer.includes("android-app://")
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ import { useBookOfflineStatus } from "@/hooks/useBookOfflineStatus";
|
|||||||
import { WifiOff } from "lucide-react";
|
import { WifiOff } from "lucide-react";
|
||||||
|
|
||||||
// Fonction utilitaire pour obtenir les informations de statut de lecture
|
// Fonction utilitaire pour obtenir les informations de statut de lecture
|
||||||
const getReadingStatusInfo = (book: KomgaBook, t: (key: string, options?: any) => string) => {
|
const getReadingStatusInfo = (
|
||||||
|
book: KomgaBook,
|
||||||
|
t: (key: string, options?: { [key: string]: string | number }) => string
|
||||||
|
) => {
|
||||||
if (!book.readProgress) {
|
if (!book.readProgress) {
|
||||||
return {
|
return {
|
||||||
label: t("books.status.unread"),
|
label: t("books.status.unread"),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, ...props }, ref) => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface TabsProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
onValueChange?: (value: string) => void;
|
onValueChange?: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TabsListProps extends React.HTMLAttributes<HTMLDivElement> {}
|
type TabsListProps = React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
|
|
||||||
export const ERROR_CODES = {
|
export const ERROR_CODES = {
|
||||||
MONGODB: {
|
MONGODB: {
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ interface PreferencesContextType {
|
|||||||
|
|
||||||
const PreferencesContext = createContext<PreferencesContextType | undefined>(undefined);
|
const PreferencesContext = createContext<PreferencesContextType | undefined>(undefined);
|
||||||
|
|
||||||
// Module-level flag to prevent duplicate fetches (survives StrictMode remounts)
|
|
||||||
let preferencesFetchInProgress = false;
|
|
||||||
let preferencesFetched = false;
|
|
||||||
|
|
||||||
export function PreferencesProvider({
|
export function PreferencesProvider({
|
||||||
children,
|
children,
|
||||||
initialPreferences,
|
initialPreferences,
|
||||||
@@ -32,58 +28,23 @@ export function PreferencesProvider({
|
|||||||
const [preferences, setPreferences] = useState<UserPreferences>(
|
const [preferences, setPreferences] = useState<UserPreferences>(
|
||||||
initialPreferences || defaultPreferences
|
initialPreferences || defaultPreferences
|
||||||
);
|
);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const isLoading = false;
|
||||||
|
|
||||||
// Check if we have valid initial preferences from server
|
// Check if we have valid initial preferences from server
|
||||||
const hasValidInitialPreferences =
|
const hasValidInitialPreferences =
|
||||||
initialPreferences && Object.keys(initialPreferences).length > 0;
|
initialPreferences && Object.keys(initialPreferences).length > 0;
|
||||||
|
|
||||||
const fetchPreferences = useCallback(async () => {
|
useEffect(() => {
|
||||||
// Prevent concurrent fetches
|
if (status === "authenticated" && hasValidInitialPreferences) {
|
||||||
if (preferencesFetchInProgress || preferencesFetched) {
|
setPreferences(initialPreferences);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
preferencesFetchInProgress = true;
|
|
||||||
|
|
||||||
try {
|
if (status === "unauthenticated") {
|
||||||
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") {
|
|
||||||
// Reset to defaults when user logs out
|
// Reset to defaults when user logs out
|
||||||
setPreferences(defaultPreferences);
|
setPreferences(defaultPreferences);
|
||||||
preferencesFetched = false; // Allow refetch on next login
|
|
||||||
}
|
}
|
||||||
}, [status, fetchPreferences, hasValidInitialPreferences]);
|
}, [status, hasValidInitialPreferences, initialPreferences]);
|
||||||
|
|
||||||
const updatePreferences = useCallback(async (newPreferences: Partial<UserPreferences>): Promise<UserPreferences | undefined> => {
|
const updatePreferences = useCallback(async (newPreferences: Partial<UserPreferences>): Promise<UserPreferences | undefined> => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export function ServiceWorkerProvider({ children }: { children: ReactNode }) {
|
|||||||
// Silently ignore message handling errors to prevent app crashes
|
// Silently ignore message handling errors to prevent app crashes
|
||||||
// This can happen with malformed messages or during SW reinstall
|
// This can happen with malformed messages or during SW reinstall
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn("[SW Context] Error handling message:", error, event.data);
|
console.warn("[SW Context] Error handling message:", error, event.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ interface KomgaUrlBuilder {
|
|||||||
params?: Record<string, string | string[]>;
|
params?: Record<string, string | string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FetchErrorLike {
|
||||||
|
code?: string;
|
||||||
|
cause?: {
|
||||||
|
code?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class BaseApiService {
|
export abstract class BaseApiService {
|
||||||
protected static async getKomgaConfig(): Promise<AuthConfig> {
|
protected static async getKomgaConfig(): Promise<AuthConfig> {
|
||||||
try {
|
try {
|
||||||
@@ -135,7 +142,7 @@ export abstract class BaseApiService {
|
|||||||
headers,
|
headers,
|
||||||
...options,
|
...options,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
// @ts-ignore - undici-specific options not in standard fetch types
|
// @ts-expect-error - undici-specific options not in standard fetch types
|
||||||
connectTimeout: timeoutMs,
|
connectTimeout: timeoutMs,
|
||||||
bodyTimeout: timeoutMs,
|
bodyTimeout: timeoutMs,
|
||||||
headersTimeout: timeoutMs,
|
headersTimeout: timeoutMs,
|
||||||
@@ -146,21 +153,21 @@ export abstract class BaseApiService {
|
|||||||
? { revalidate: options.revalidate }
|
? { revalidate: options.revalidate }
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
} catch (fetchError: any) {
|
} catch (fetchError: unknown) {
|
||||||
|
const normalizedError = fetchError as FetchErrorLike;
|
||||||
// Gestion spécifique des erreurs DNS
|
// Gestion spécifique des erreurs DNS
|
||||||
if (fetchError?.cause?.code === "EAI_AGAIN" || fetchError?.code === "EAI_AGAIN") {
|
if (normalizedError.cause?.code === "EAI_AGAIN" || normalizedError.code === "EAI_AGAIN") {
|
||||||
logger.error(`DNS resolution failed for ${url}. Retrying with different DNS settings...`);
|
logger.error(`DNS resolution failed for ${url}. Retrying with different DNS settings...`);
|
||||||
|
|
||||||
response = await fetch(url, {
|
response = await fetch(url, {
|
||||||
headers,
|
headers,
|
||||||
...options,
|
...options,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
// @ts-ignore - undici-specific options
|
// @ts-expect-error - undici-specific options
|
||||||
connectTimeout: timeoutMs,
|
connectTimeout: timeoutMs,
|
||||||
bodyTimeout: timeoutMs,
|
bodyTimeout: timeoutMs,
|
||||||
headersTimeout: timeoutMs,
|
headersTimeout: timeoutMs,
|
||||||
// Force IPv4 si IPv6 pose problème
|
// Force IPv4 si IPv6 pose problème
|
||||||
// @ts-ignore
|
|
||||||
family: 4,
|
family: 4,
|
||||||
// Next.js cache with tags support
|
// Next.js cache with tags support
|
||||||
next: options.tags
|
next: options.tags
|
||||||
@@ -169,7 +176,7 @@ export abstract class BaseApiService {
|
|||||||
? { revalidate: options.revalidate }
|
? { revalidate: options.revalidate }
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
} else if (fetchError?.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
|
} else if (normalizedError.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
|
||||||
// Retry automatique sur timeout de connexion (cold start)
|
// Retry automatique sur timeout de connexion (cold start)
|
||||||
logger.info(`⏱️ Connection timeout for ${url}. Retrying once (cold start)...`);
|
logger.info(`⏱️ Connection timeout for ${url}. Retrying once (cold start)...`);
|
||||||
|
|
||||||
@@ -177,7 +184,7 @@ export abstract class BaseApiService {
|
|||||||
headers,
|
headers,
|
||||||
...options,
|
...options,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
// @ts-ignore - undici-specific options
|
// @ts-expect-error - undici-specific options
|
||||||
connectTimeout: timeoutMs,
|
connectTimeout: timeoutMs,
|
||||||
bodyTimeout: timeoutMs,
|
bodyTimeout: timeoutMs,
|
||||||
headersTimeout: timeoutMs,
|
headersTimeout: timeoutMs,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { PreferencesService } from "./preferences.service";
|
|||||||
import { ERROR_CODES } from "../../constants/errorCodes";
|
import { ERROR_CODES } from "../../constants/errorCodes";
|
||||||
import { AppError } from "../../utils/errors";
|
import { AppError } from "../../utils/errors";
|
||||||
|
|
||||||
|
type ErrorWithStatusParams = AppError & { params?: { status?: number } };
|
||||||
|
|
||||||
export class BookService extends BaseApiService {
|
export class BookService extends BaseApiService {
|
||||||
private static readonly CACHE_TTL = 60; // 1 minute
|
private static readonly CACHE_TTL = 60; // 1 minute
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ export class BookService extends BaseApiService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
book,
|
book,
|
||||||
pages: pages.map((page: any) => page.number),
|
pages: pages.map((page) => page.number),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {}, error);
|
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {}, error);
|
||||||
@@ -43,7 +45,7 @@ export class BookService extends BaseApiService {
|
|||||||
if (
|
if (
|
||||||
error instanceof AppError &&
|
error instanceof AppError &&
|
||||||
error.code === ERROR_CODES.KOMGA.HTTP_ERROR &&
|
error.code === ERROR_CODES.KOMGA.HTTP_ERROR &&
|
||||||
(error as any).params?.status === 404
|
(error as ErrorWithStatusParams).params?.status === 404
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ interface KomgaLibraryRaw {
|
|||||||
unavailable: boolean;
|
unavailable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KomgaCondition = Record<string, unknown>;
|
||||||
|
|
||||||
export class LibraryService extends BaseApiService {
|
export class LibraryService extends BaseApiService {
|
||||||
private static readonly CACHE_TTL = 300; // 5 minutes
|
private static readonly CACHE_TTL = 300; // 5 minutes
|
||||||
|
|
||||||
@@ -83,7 +85,7 @@ export class LibraryService extends BaseApiService {
|
|||||||
const headers = { "Content-Type": "application/json" };
|
const headers = { "Content-Type": "application/json" };
|
||||||
|
|
||||||
// Construction du body de recherche pour Komga
|
// Construction du body de recherche pour Komga
|
||||||
let condition: any;
|
let condition: KomgaCondition;
|
||||||
|
|
||||||
if (unreadOnly) {
|
if (unreadOnly) {
|
||||||
condition = {
|
condition = {
|
||||||
@@ -101,7 +103,7 @@ export class LibraryService extends BaseApiService {
|
|||||||
condition = { libraryId: { operator: "is", value: libraryId } };
|
condition = { libraryId: { operator: "is", value: libraryId } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchBody: { condition: any; fullTextSearch?: string } = { condition };
|
const searchBody: { condition: KomgaCondition; fullTextSearch?: string } = { condition };
|
||||||
|
|
||||||
const params: Record<string, string | string[]> = {
|
const params: Record<string, string | string[]> = {
|
||||||
page: String(page),
|
page: String(page),
|
||||||
|
|||||||
@@ -55,13 +55,15 @@ export class PreferencesService {
|
|||||||
const user = await this.getCurrentUser();
|
const user = await this.getCurrentUser();
|
||||||
const userId = parseInt(user.id, 10);
|
const userId = parseInt(user.id, 10);
|
||||||
|
|
||||||
const updateData: Record<string, any> = {};
|
const updateData: Prisma.PreferencesUpdateInput = {};
|
||||||
if (preferences.showThumbnails !== undefined)
|
if (preferences.showThumbnails !== undefined)
|
||||||
updateData.showThumbnails = preferences.showThumbnails;
|
updateData.showThumbnails = preferences.showThumbnails;
|
||||||
if (preferences.showOnlyUnread !== undefined)
|
if (preferences.showOnlyUnread !== undefined)
|
||||||
updateData.showOnlyUnread = preferences.showOnlyUnread;
|
updateData.showOnlyUnread = preferences.showOnlyUnread;
|
||||||
if (preferences.displayMode !== undefined) updateData.displayMode = preferences.displayMode;
|
if (preferences.displayMode !== undefined) updateData.displayMode = preferences.displayMode;
|
||||||
if (preferences.background !== undefined) updateData.background = preferences.background;
|
if (preferences.background !== undefined) {
|
||||||
|
updateData.background = preferences.background as unknown as Prisma.InputJsonValue;
|
||||||
|
}
|
||||||
if (preferences.readerPrefetchCount !== undefined)
|
if (preferences.readerPrefetchCount !== undefined)
|
||||||
updateData.readerPrefetchCount = preferences.readerPrefetchCount;
|
updateData.readerPrefetchCount = preferences.readerPrefetchCount;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ type PendingRequest<T> = Promise<T>;
|
|||||||
|
|
||||||
class RequestDeduplicationService {
|
class RequestDeduplicationService {
|
||||||
// Map pour tracker les requêtes en cours par clé unique
|
// Map pour tracker les requêtes en cours par clé unique
|
||||||
private pendingRequests = new Map<string, PendingRequest<any>>();
|
private pendingRequests = new Map<string, PendingRequest<unknown>>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exécute une requête de manière dédupliquée
|
* Exécute une requête de manière dédupliquée
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { AppError } from "../../utils/errors";
|
|||||||
import type { UserPreferences } from "@/types/preferences";
|
import type { UserPreferences } from "@/types/preferences";
|
||||||
import logger from "@/lib/logger";
|
import logger from "@/lib/logger";
|
||||||
|
|
||||||
|
type KomgaCondition = Record<string, unknown>;
|
||||||
|
|
||||||
export class SeriesService extends BaseApiService {
|
export class SeriesService extends BaseApiService {
|
||||||
private static readonly CACHE_TTL = 120; // 2 minutes
|
private static readonly CACHE_TTL = 120; // 2 minutes
|
||||||
|
|
||||||
@@ -34,7 +36,7 @@ export class SeriesService extends BaseApiService {
|
|||||||
const headers = { "Content-Type": "application/json" };
|
const headers = { "Content-Type": "application/json" };
|
||||||
|
|
||||||
// Construction du body de recherche pour Komga
|
// Construction du body de recherche pour Komga
|
||||||
let condition: any;
|
let condition: KomgaCondition;
|
||||||
|
|
||||||
if (unreadOnly) {
|
if (unreadOnly) {
|
||||||
// Utiliser allOf pour combiner seriesId avec anyOf pour UNREAD ou IN_PROGRESS
|
// Utiliser allOf pour combiner seriesId avec anyOf pour UNREAD ou IN_PROGRESS
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ export function formatDate(date: string | Date): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function debounce<T extends (...args: any[]) => void>(
|
export function debounce<TArgs extends unknown[]>(
|
||||||
func: T,
|
func: (...args: TArgs) => void,
|
||||||
wait: number
|
wait: number
|
||||||
): (...args: Parameters<T>) => void {
|
): (...args: TArgs) => void {
|
||||||
let timeout: NodeJS.Timeout;
|
let timeout: NodeJS.Timeout;
|
||||||
|
|
||||||
return function executedFunction(...args: Parameters<T>) {
|
return function executedFunction(...args: TArgs) {
|
||||||
const later = () => {
|
const later = () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
func(...args);
|
func(...args);
|
||||||
|
|||||||
2
src/types/json.d.ts
vendored
2
src/types/json.d.ts
vendored
@@ -1,4 +1,4 @@
|
|||||||
declare module "*.json" {
|
declare module "*.json" {
|
||||||
const value: any;
|
const value: unknown;
|
||||||
export default value;
|
export default value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export function findHttpStatus(error: unknown): number | null {
|
|||||||
|
|
||||||
// Si c'est une erreur HTTP, récupérer le status
|
// Si c'est une erreur HTTP, récupérer le status
|
||||||
if (error.code === ERROR_CODES.KOMGA.HTTP_ERROR) {
|
if (error.code === ERROR_CODES.KOMGA.HTTP_ERROR) {
|
||||||
return (error.params as any)?.status || null;
|
const params = error.params as { status?: number } | undefined;
|
||||||
|
return params?.status || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sinon, chercher récursivement dans originalError
|
// Sinon, chercher récursivement dans originalError
|
||||||
|
|||||||
52
thoughts/reviews/api-get-cleanup-review.md
Normal file
52
thoughts/reviews/api-get-cleanup-review.md
Normal file
@@ -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.
|
||||||
Reference in New Issue
Block a user