diff --git a/PLAN_OPTIMISATION_PERFORMANCES.md b/PLAN_OPTIMISATION_PERFORMANCES.md deleted file mode 100644 index d61d802..0000000 --- a/PLAN_OPTIMISATION_PERFORMANCES.md +++ /dev/null @@ -1,401 +0,0 @@ -# Plan d'Optimisation des Performances - StripStream - -## 🔴 Problèmes Identifiés - -### Problème Principal : Pagination côté client au lieu de Komga - -**Code actuel problématique :** - -```typescript -// library.service.ts - ligne 59 -size: "5000"; // Récupère TOUTES les séries d'un coup - -// series.service.ts - ligne 69 -size: "1000"; // Récupère TOUS les livres d'un coup -``` - -**Impact :** - -- Charge massive en mémoire (stocker 5000 séries) -- Temps de réponse longs (transfert de gros JSON) -- Cache volumineux et inefficace -- Pagination manuelle côté serveur Node.js - -### Autres Problèmes - -1. **TRIPLE cache conflictuel** - - - **Service Worker** : Cache les données API dans `DATA_CACHE` avec SWR - - **ServerCacheService** : Cache côté serveur avec SWR - - **Headers HTTP** : `Cache-Control` sur les routes API - - Comportements imprévisibles, données désynchronisées - -2. **Clés de cache trop larges** - - - `library-{id}-all-series` → stocke TOUT - - Pas de clé par page/filtres - -3. **Préférences rechargées à chaque requête** - - - `PreferencesService.getPreferences()` fait une query DB à chaque fois - - Pas de mise en cache des préférences - -4. **ISR mal configuré** - - `export const revalidate = 60` sur routes dynamiques - - Conflit avec le cache serveur - ---- - -## ✅ Plan de Développement - -### Phase 1 : Pagination Native Komga (PRIORITÉ HAUTE) - -- [x] **1.1 Refactorer `LibraryService.getLibrarySeries()`** - - - Utiliser directement la pagination Komga - - Endpoint: `POST /api/v1/series/list?page={page}&size={size}` - - Supprimer `getAllLibrarySeries()` et le slice manuel - - Passer les filtres (unread, search) directement à Komga - -- [x] **1.2 Refactorer `SeriesService.getSeriesBooks()`** - - - Utiliser directement la pagination Komga - - Endpoint: `POST /api/v1/books/list?page={page}&size={size}` - - Supprimer `getAllSeriesBooks()` et le slice manuel (gardée pour book.service.ts) - -- [x] **1.3 Adapter les clés de cache** - - - Clé incluant page + size + filtres - - Format: `library-{id}-series-p{page}-s{size}-u{unread}-q{search}` ✅ - - Format: `series-{id}-books-p{page}-s{size}-u{unread}` ✅ - -- [x] **1.4 Mettre à jour les routes API** - - `/api/komga/libraries/[libraryId]/series` ✅ (utilise déjà `LibraryService.getLibrarySeries()` refactoré) - - `/api/komga/series/[seriesId]/books` ✅ (utilise déjà `SeriesService.getSeriesBooks()` refactoré) - -### Phase 2 : Simplification du Cache (Triple → Simple) - -**Objectif : Passer de 3 couches de cache à 1 seule (ServerCacheService)** - -- [x] **2.1 Désactiver le cache SW pour les données API** - - - Modifier `sw.js` : retirer le cache des routes `/api/komga/*` (sauf images) - - Garder uniquement le cache SW pour : images, static, navigation - - Le cache serveur suffit pour les données - -- [x] **2.2 Supprimer les headers HTTP Cache-Control** - - - Retirer `Cache-Control` des NextResponse dans les routes API - - Évite les conflits avec le cache serveur - - Note: Conservé pour les images de pages de livres (max-age=31536000) - -- [x] **2.3 Supprimer `revalidate` des routes dynamiques** - - - Routes API = dynamiques, pas besoin d'ISR - - Le cache serveur suffit - -- [x] **2.4 Optimiser les TTL ServerCacheService** - - Réduire TTL des listes paginées (2 min) ✅ - - Garder TTL court pour les données avec progression (2 min) ✅ - - Garder TTL long pour les images (7 jours) ✅ - -**Résultat final :** - -| Type de donnée | Cache utilisé | Stratégie | -| ---------------- | ------------------ | ------------- | -| Images | SW (IMAGES_CACHE) | Cache-First | -| Static (\_next/) | SW (STATIC_CACHE) | Cache-First | -| Données API | ServerCacheService | SWR | -| Navigation | SW | Network-First | - -### Phase 3 : Optimisation des Préférences - -- [ ] **3.1 Cacher les préférences utilisateur** - - - Créer `PreferencesService.getCachedPreferences()` - - TTL court (1 minute) - - Invalidation manuelle lors des modifications - -- [ ] **3.2 Réduire les appels DB** - - Grouper les appels de config Komga + préférences - - Request-level caching (par requête HTTP) - -### Phase 4 : Optimisation du Home - -- [ ] **4.1 Paralléliser intelligemment les appels Komga** - - - Les 5 appels sont déjà en parallèle ✅ - - Vérifier que le circuit breaker ne bloque pas - -- [ ] **4.2 Réduire la taille des données Home** - - Utiliser des projections (ne récupérer que les champs nécessaires) - - Limiter à 10 items par section (déjà fait ✅) - -### Phase 5 : Nettoyage et Simplification - -- [ ] **5.1 Supprimer le code mort** - - - `getAllLibrarySeries()` (après phase 1) - - `getAllSeriesBooks()` (après phase 1) - -- [ ] **5.2 Documenter la nouvelle architecture** - - - Mettre à jour `docs/caching.md` - - Documenter les nouvelles clés de cache - -- [ ] **5.3 Ajouter des métriques** - - Temps de réponse des requêtes Komga - - Hit/Miss ratio du cache - - Taille des payloads - ---- - -## 📝 Implémentation Détaillée - -### Phase 1.1 : Nouveau `LibraryService.getLibrarySeries()` - -```typescript -static async getLibrarySeries( - libraryId: string, - page: number = 0, - size: number = 20, - unreadOnly: boolean = false, - search?: string -): Promise> { - const headers = { "Content-Type": "application/json" }; - - // Construction du body de recherche pour Komga - const condition: Record = { - libraryId: { operator: "is", value: libraryId }, - }; - - // Filtre unread natif Komga - if (unreadOnly) { - condition.readStatus = { operator: "is", value: "IN_PROGRESS" }; - // OU utiliser: complete: { operator: "is", value: false } - } - - const searchBody = { condition }; - - // Clé de cache incluant tous les paramètres - const cacheKey = `library-${libraryId}-series-p${page}-s${size}-u${unreadOnly}-q${search || ''}`; - - const response = await this.fetchWithCache>( - cacheKey, - async () => { - const params: Record = { - page: String(page), - size: String(size), - sort: "metadata.titleSort,asc", - }; - - // Filtre de recherche - if (search) { - params.search = search; - } - - return this.fetchFromApi>( - { path: "series/list", params }, - headers, - { method: "POST", body: JSON.stringify(searchBody) } - ); - }, - "SERIES" - ); - - // Filtrer les séries supprimées côté client (léger) - response.content = response.content.filter((series) => !series.deleted); - - return response; -} -``` - -### Phase 1.2 : Nouveau `SeriesService.getSeriesBooks()` - -```typescript -static async getSeriesBooks( - seriesId: string, - page: number = 0, - size: number = 24, - unreadOnly: boolean = false -): Promise> { - const headers = { "Content-Type": "application/json" }; - - const condition: Record = { - seriesId: { operator: "is", value: seriesId }, - }; - - if (unreadOnly) { - condition.readStatus = { operator: "isNot", value: "READ" }; - } - - const searchBody = { condition }; - - const cacheKey = `series-${seriesId}-books-p${page}-s${size}-u${unreadOnly}`; - - const response = await this.fetchWithCache>( - cacheKey, - async () => - this.fetchFromApi>( - { - path: "books/list", - params: { - page: String(page), - size: String(size), - sort: "number,asc", - }, - }, - headers, - { method: "POST", body: JSON.stringify(searchBody) } - ), - "BOOKS" - ); - - // Filtrer les livres supprimés côté client (léger) - response.content = response.content.filter((book) => !book.deleted); - - return response; -} -``` - -### Phase 2.1 : Modification du Service Worker - -```javascript -// sw.js - SUPPRIMER cette section -// Route 3: API data → Stale-While-Revalidate (if cacheable) -// if (isApiDataRequest(url.href) && shouldCacheApiData(url.href)) { -// event.respondWith(staleWhileRevalidateStrategy(request, DATA_CACHE)); -// return; -// } - -// Garder uniquement : -// - Route 1: Images → Cache-First -// - Route 2: RSC payloads → Stale-While-Revalidate (pour navigation) -// - Route 4: Static → Cache-First -// - Route 5: Navigation → Network-First -``` - -**Pourquoi supprimer le cache SW des données API ?** - -- Le ServerCacheService fait déjà du SWR côté serveur -- Pas de bénéfice à cacher 2 fois -- Simplifie l'invalidation (un seul endroit) -- Les données restent accessibles en mode online via ServerCache - -### Phase 2.2 : Routes API simplifiées - -```typescript -// libraries/[libraryId]/series/route.ts -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ libraryId: string }> } -) { - const libraryId = (await params).libraryId; - const searchParams = request.nextUrl.searchParams; - - const page = parseInt(searchParams.get("page") || "0"); - const size = parseInt(searchParams.get("size") || "20"); - 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), - ]); - - // Plus de headers Cache-Control ! - return NextResponse.json({ series, library }); -} - -// Supprimer: export const revalidate = 60; -``` - ---- - -## 📊 Gains Attendus - -| Métrique | Avant | Après (estimé) | -| ------------------------- | ------------ | -------------- | -| Payload initial Library | ~500KB - 5MB | ~10-50KB | -| Temps 1ère page Library | 2-10s | 200-500ms | -| Mémoire cache par library | ~5MB | ~50KB/page | -| Requêtes Komga par page | 1 grosse | 1 petite | - ---- - -## ⚠️ Impact sur le Mode Offline - -**Avant (triple cache) :** - -- Données API cachées par le SW → navigation offline possible - -**Après (cache serveur uniquement) :** - -- Données API non cachées côté client -- Mode offline limité aux images déjà vues -- Page offline.html affichée si pas de connexion - -**Alternative si offline critique :** - -- Option 1 : Garder le cache SW uniquement pour les pages "Home" et "Library" visitées -- Option 2 : Utiliser IndexedDB pour un vrai mode offline (plus complexe) -- Option 3 : Accepter la limitation (majoritaire pour un reader de comics) - ---- - -## 🔧 Tests à Effectuer - -- [ ] Test pagination avec grande bibliothèque (>1000 séries) -- [ ] Test filtres (unread, search) avec pagination -- [ ] Test changement de page rapide (pas de race conditions) -- [ ] Test invalidation cache (refresh) -- [ ] Test mode offline → vérifier que offline.html s'affiche -- [ ] Test images offline → doivent rester accessibles - ---- - -## 📅 Ordre de Priorité - -1. **Urgent** : Phase 1 (pagination native) - Impact maximal -2. **Important** : Phase 2 (simplification cache) - Évite les bugs -3. **Moyen** : Phase 3 (préférences) - Optimisation secondaire -4. **Faible** : Phase 4-5 (nettoyage) - Polish - ---- - -## Notes Techniques - -### API Komga - Pagination - -L'API Komga supporte nativement : - -- `page` : Index de page (0-based) -- `size` : Nombre d'éléments par page -- `sort` : Tri (ex: `metadata.titleSort,asc`) - -Endpoint POST `/api/v1/series/list` accepte un body avec `condition` pour filtrer. - -### Filtres Komga disponibles - -```json -{ - "condition": { - "libraryId": { "operator": "is", "value": "xxx" }, - "readStatus": { "operator": "is", "value": "IN_PROGRESS" }, - "complete": { "operator": "is", "value": false } - } -} -``` - -### Réponse paginée Komga - -```json -{ - "content": [...], - "pageable": { "pageNumber": 0, "pageSize": 20 }, - "totalElements": 150, - "totalPages": 8, - "first": true, - "last": false -} -``` diff --git a/docs/komga-api-summary.md b/docs/komga-api-summary.md new file mode 100644 index 0000000..4f33b0e --- /dev/null +++ b/docs/komga-api-summary.md @@ -0,0 +1,75 @@ +# Résumé Spec Komga OpenAPI v1.24.1 + +## Authentication +- **Basic Auth** ou **API Key** (`X-API-Key` header) +- Sessions: cookie `KOMGA-SESSION` ou header `X-Auth-Token` +- "Remember me" supporté + +## Endpoints Principaux + +### Libraries +| Méthode | Endpoint | Description | +|--------|----------|-------------| +| GET | `/libraries` | Liste des bibliothèques | +| GET | `/libraries/{id}` | Détail d'une bibliothèque | + +### Series +| Méthode | Endpoint | Description | +|--------|----------|-------------| +| GET | `/series` | Liste des séries (GET avec params) | +| POST | `/series/list` | Liste paginée avec filtres (JSON body) | +| GET | `/series/{id}` | Détail d'une série | +| GET | `/series/{id}/thumbnail` | Vignette (image complète) | +| GET | `/series/{id}/books` | Livres d'une série | + +### Books +| Méthode | Endpoint | Description | +|--------|----------|-------------| +| GET | `/books` | Liste des livres | +| POST | `/books/list` | Liste paginée avec filtres | +| GET | `/books/{id}` | Détail d'un livre | +| GET | `/books/{id}/pages` | Liste des pages | +| GET | `/books/{id}/pages/{n}` | Image d'une page (streaming) | +| GET | `/books/{id}/pages/{n}/thumbnail` | Miniature (300px max) | + +### Collections & Readlists +| Méthode | Endpoint | Description | +|--------|----------|-------------| +| GET/POST | `/collections` | CRUD Collections | +| GET/POST | `/readlists` | CRUD Readlists | + +## Pagination + +**Paramètres query:** +- `page` - Index 0-based +- `size` - Taille de page +- `sort` - Tri (ex: `metadata.titleSort,asc`) + +**Corps JSON (POST):** +```json +{ + "condition": { + "libraryId": { "operator": "is", "value": "xxx" } + }, + "fullTextSearch": "query" +} +``` + +## Opérateurs +- `is`, `isNot` +- `contains`, `containsNot` +- `before`, `after`, `beforeOrEqual`, `afterOrEqual` + +## Opérateurs Logiques +- `allOf` - ET logique +- `anyOf` - OU logique + +## Images + +| Type | Endpoint | Taille | +|------|----------|--------| +| Vignette série | `/series/{id}/thumbnail` | Taille originale | +| Page livre | `/books/{id}/pages/{n}` | Taille originale (streaming) | +| Miniature page | `/books/{id}/pages/{n}/thumbnail` | 300px max | + +**Note:** Komga ne fournit pas de redimensionnement pour les vignettes de séries. diff --git a/docs/plan-optimisation.md b/docs/plan-optimisation.md new file mode 100644 index 0000000..e9e975d --- /dev/null +++ b/docs/plan-optimisation.md @@ -0,0 +1,146 @@ +# Plan d'Optimisation des Performances + +> Dernière mise à jour: 2026-02-27 + +## État Actuel + +### Ce qui fonctionne bien +- ✅ Pagination native Komga (`POST /series/list`, `POST /books/list`) +- ✅ Prefetching des pages de livre (avec déduplication) +- ✅ 5 appels parallèles pour la page Home +- ✅ Service Worker pour images et navigation +- ✅ Timeout et retry sur les appels API +- ✅ Prisma singleton pour éviter les connexions multiples +- ✅ **Cache serveur API avec Next.js revalidate** (ajouté) + +--- + +## Analyse Complète + +### ⚡ Problèmes de Performance + +#### 🔴 Critique - N+1 API Calls dans getLibraries +**Impact:** Fort | **Fichiers:** `src/lib/services/library.service.ts:22-46` + +Les appels pour récupérer le count des livres sont parallèles (Promise.all), mais on pourrait utiliser l'endpoint Komga `expand=booksCount` si disponible. + +#### 🟡 Cache préférences (IMPACT: MOYEN) +**Symptôme:** Chaque lecture de préférences = 1 query DB + +**Fichier:** `src/lib/services/preferences.service.ts` + +--- + +### 🔒 Problemes de Securite + +#### 🔴 Critique - Auth Header en clair +**Impact:** HIGH | **Fichiers:** `src/lib/services/config-db.service.ts:21-23` + +```typescript +// Problème: authHeader stocké en clair dans la DB +const authHeader: string = Buffer.from(`${data.username}:${data.password}`).toString("base64"); +``` + +**Solution:** Chiffrer avec AES-256 avant de stocker. Ajouter `ENCRYPTION_KEY` dans .env + +#### 🔴 Pas de rate limiting +**Impact:** HIGH | **Fichiers:** Toutes les routes API + +**Solution:** Ajouter `rate-limiter-flexible` pour limiter les requêtes par IP/user + +#### 🟡 Pas de sanitization des inputs +**Impact:** MEDIUM | **Fichiers:** `library.service.ts`, `series.service.ts` + +--- + +### ⚠️ Autres Problemes + +#### Service Worker double-cache (IMPACT: FAIBLE) +**Symptôme:** Conflit entre cache SW et navigateur + +**Fichier:** `public/sw.js` (ligne 528-536) + +#### RequestDeduplicationService non utilise +**Impact:** Moyen | **Fichier:** `src/lib/services/request-deduplication.service.ts` + +#### getHomeData echoue completement si une requete echoue +**Impact:** Fort | **Fichier:** `src/app/api/komga/home/route.ts` + +--- + +## Priorites d'implementation + +### ✅ Phase 2: Performance (COMPLETEE) +- **Cache serveur API via fetchFromApi avec option `revalidate`** +- Fichiers modifiés: + - `src/lib/services/base-api.service.ts` - ajout option `revalidate` dans fetch + - `src/lib/services/library.service.ts` - CACHE_TTL = 300s (5 min) + - `src/lib/services/home.service.ts` - CACHE_TTL = 120s (2 min) + - `src/lib/services/series.service.ts` - CACHE_TTL = 120s (2 min) + - `src/lib/services/book.service.ts` - CACHE_TTL = 60s (1 min) + +### Phase 1: Securite (Priorite HAUTE) + +1. **Chiffrer les identifiants Komga** + ```typescript + // src/lib/utils/encryption.ts + import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'; + + const ALGORITHM = 'aes-256-gcm'; + const key = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex'); + + export function encrypt(text: string): string { + const iv = randomBytes(16); + const cipher = createCipheriv(ALGORITHM, key, iv); + const encrypted = Buffer.concat([cipher.update(text), cipher.final()]); + return `${iv.toString('hex')}:${encrypted.toString('hex')}:${cipher.getAuthTag().toString('hex')}`; + } + ``` + +2. **Ajouter rate limiting** + +### Phase 3: Fiabilite (Priorite MOYENNE) + +1. **Graceful degradation** sur Home (afficher données partielles si un appel échoue) +2. **Cache préférences** + +### Phase 4: Nettoyage (Priorite FAIBLE) + +1. **Supprimer double-cache SW** pour les données API + +--- + +## TTL Recommandes pour Cache + +| Donnee | TTL | Status | +|--------|-----|--------| +| Home | 2 min | ✅ Implementé | +| Series list | 2 min | ✅ Implementé | +| Books list | 2 min | ✅ Implementé | +| Book details | 1 min | ✅ Implementé | +| Libraries | 5 min | ✅ Implementé | +| Preferences | - | ⏳ Non fait | + +--- + +## Fichiers a Modifier + +### ✅ Performance (COMPLET) +1. ~~`src/lib/services/server-cache.service.ts`~~ - Supprimé, on utilise Next.js natif +2. ~~`src/lib/services/base-api.service.ts`~~ - Ajout option `revalidate` dans fetch +3. ~~`src/lib/services/home.service.ts`~~ - CACHE_TTL = 120s +4. ~~`src/lib/services/library.service.ts`~~ - CACHE_TTL = 300s +5. ~~`src/lib/services/series.service.ts`~~ - CACHE_TTL = 120s +6. ~~`src/lib/services/book.service.ts`~~ - CACHE_TTL = 60s + +### 🔒 Securite (A FAIRE) +1. `src/lib/utils/encryption.ts` (nouveau) +2. `src/lib/services/config-db.service.ts` - Utiliser chiffrement +3. `src/middleware.ts` - Ajouter rate limiting + +### Fiabilite (A FAIRE) +1. `src/app/api/komga/home/route.ts` - Graceful degradation +2. `src/lib/services/base-api.service.ts` - Utiliser deduplication + +### Nettoyage (A FAIRE) +1. `public/sw.js` - Supprimer cache API diff --git a/project-intelligence/business-domain.md b/project-intelligence/business-domain.md new file mode 100644 index 0000000..06e57b9 --- /dev/null +++ b/project-intelligence/business-domain.md @@ -0,0 +1,65 @@ + + +# Business Domain + +**Purpose**: Business context, problems solved, and value created for the comic/manga reader app. +**Last Updated**: 2026-02-27 + +## Quick Reference +- **Update When**: Business direction changes, new features shipped +- **Audience**: Developers needing context, stakeholders + +## Project Identity +``` +Project Name: Stripstream +Tagline: Modern web reader for digital comics and manga +Problem: Need a responsive, feature-rich web interface for reading comics from Komga servers +Solution: Next.js PWA that syncs with Komga API, supports offline reading, multiple reading modes +``` + +## Target Users +| Segment | Who | Needs | Pain Points | +|---------|-----|-------|--------------| +| Comic/Manga Readers | Users with Komga servers | Read comics in browser | Komga web UI is limited | +| Mobile Readers | iPad/Android users | Offline reading, touch gestures | No native mobile app | +| Library Organizers | Users with large collections | Search, filter, track progress | Hard to manage | + +## Value Proposition +**For Users**: +- Read comics anywhere via responsive PWA +- Offline reading with local storage +- Multiple viewing modes (single/double page, RTL, scroll) +- Progress sync with Komga server +- Light/Dark mode, language support (EN/FR) + +**For Business**: +- Open source showcase +- Demonstrates Next.js + Komga integration patterns + +## Key Features +- **Sync**: Read progress, series/books lists with Komga +- **Reader**: RTL, double/single page, zoom, thumbnails, fullscreen +- **Offline**: PWA with local book downloads +- **UI**: Dark/light, responsive, loading/error states +- **Lists**: Pagination, search, mark read/unread, favorites +- **Settings**: Cache TTL, Komga config, display preferences + +## Tech Stack Context +- Integrates with **Komga** (comic server API) +- Uses **MongoDB** for local caching/preferences +- **NextAuth** for authentication (session-based) + +## Success Metrics +| Metric | Target | +|--------|--------| +| Page load | <2s | +| Reader responsiveness | 60fps | +| PWA install rate | Track via analytics | + +## Constraints +- Requires Komga server (not standalone) +- Mobile storage limits for offline books + +## Related Files +- `technical-domain.md` - Tech stack and code patterns +- `business-tech-bridge.md` - Business-technical mapping diff --git a/project-intelligence/business-tech-bridge.md b/project-intelligence/business-tech-bridge.md new file mode 100644 index 0000000..0e24b2f --- /dev/null +++ b/project-intelligence/business-tech-bridge.md @@ -0,0 +1,65 @@ + + +# Business-Tech Bridge + +**Purpose**: Map business concepts to technical implementation. +**Last Updated**: 2026-02-27 + +## Quick Reference +- **Update When**: New features that bridge business and tech +- **Audience**: Developers, product + +--- + +## Business → Technical Mapping + +| Business Concept | Technical Implementation | +|-----------------|------------------------| +| Read comics | `BookService.getBook()`, PhotoswipeReader component | +| Sync progress | Komga API calls in `ReadProgressService` | +| Offline reading | Service Worker + IndexedDB via `ClientOfflineBookService` | +| User preferences | MongoDB + `PreferencesService` | +| Library management | `LibraryService`, `SeriesService` | +| Authentication | NextAuth v5 + `AuthServerService` | + +--- + +## User Flows → API Routes + +| User Action | API Route | Service | +|-------------|-----------|---------| +| View home | `GET /api/komga/home` | `HomeService` | +| Browse series | `GET /api/komga/libraries/:id/series` | `SeriesService` | +| Read book | `GET /api/komga/books/:id` | `BookService` | +| Update progress | `POST /api/komga/books/:id/read-progress` | `BookService` | +| Download book | `GET /api/komga/images/books/:id/pages/:n` | `ImageService` | + +--- + +## Components → Services + +| UI Component | Service Layer | +|--------------|---------------| +| HomeContent | HomeService | +| SeriesGrid | SeriesService | +| BookCover | BookService | +| PhotoswipeReader | ImageService | +| FavoritesButton | FavoriteService | +| SettingsPanel | PreferencesService | + +--- + +## Data Flow + +``` +User Request → API Route → Service → Komga API / MongoDB → Response + ↓ + Logger (Pino) +``` + +--- + +## Related Files +- `technical-domain.md` - Tech stack and code patterns +- `business-domain.md` - Business context +- `decisions-log.md` - Architecture decisions diff --git a/project-intelligence/decisions-log.md b/project-intelligence/decisions-log.md new file mode 100644 index 0000000..b585b68 --- /dev/null +++ b/project-intelligence/decisions-log.md @@ -0,0 +1,130 @@ + + +# Decisions Log + +**Purpose**: Record architecture decisions with context and rationale. +**Last Updated**: 2026-02-27 + +## Quick Reference +- **Update When**: New architecture decisions +- **Audience**: Developers, architects + +--- + +## ADR-001: Use Prisma with MongoDB + +**Date**: 2024 +**Status**: Accepted + +**Context**: Need database for caching Komga responses and storing user preferences. + +**Decision**: Use Prisma ORM with MongoDB adapter. + +**Rationale**: +- Type-safe queries across the app +- Schema migration support +- Works well with MongoDB's flexible schema + +**Alternatives Considered**: +- Mongoose: Less type-safe, manual schema management +- Raw MongoDB driver: No type safety, verbose + +--- + +## ADR-002: Service Layer Pattern + +**Date**: 2024 +**Status**: Accepted + +**Context**: API routes need business logic separated from HTTP handling. + +**Decision**: Create service classes in `src/lib/services/` (BookService, SeriesService, etc.) + +**Rationale**: +- Separation of concerns +- Testable business logic +- Reusable across API routes + +**Example**: +```typescript +// API route (thin) +export async function GET(request: NextRequest, { params }) { + const book = await BookService.getBook(bookId); + return NextResponse.json(book); +} + +// Service (business logic) +class BookService { + static async getBook(bookId: string) { ... } +} +``` + +--- + +## ADR-003: Custom AppError with Error Codes + +**Date**: 2024 +**Status**: Accepted + +**Context**: Need consistent error handling across API. + +**Decision**: Custom `AppError` class with error codes from `ERROR_CODES` constant. + +**Rationale**: +- Consistent error format: `{ error: { code, name, message } }` +- Typed error codes for client handling +- Centralized error messages via `getErrorMessage()` + +--- + +## ADR-004: Radix UI + Tailwind for Components + +**Date**: 2024 +**Status**: Accepted + +**Context**: Need accessible UI components without fighting a component library. + +**Decision**: Use Radix UI primitives with custom Tailwind styling. + +**Rationale**: +- Radix provides accessible primitives +- Full control over styling via Tailwind +- Shadcn-like pattern (cva + cn) + +--- + +## ADR-005: Client-Side Request Deduplication + +**Date**: 2024 +**Status**: Accepted + +**Context**: Multiple components may request same data (e.g., home page with series, books, continue reading). + +**Decision**: `RequestDeduplicationService` with React query-like deduplication. + +**Rationale**: +- Reduces Komga API calls +- Consistent data across components +- Configurable TTL + +--- + +## ADR-006: PWA with Offline Book Storage + +**Date**: 2024 +**Status**: Accepted + +**Context**: Users want to read offline, especially on mobile. + +**Decision**: Next PWA + Service Worker + IndexedDB for storing book blobs. + +**Rationale**: +- Full offline capability +- Background sync when online +- Local storage limits on mobile + +--- + +## Related Files +- `technical-domain.md` - Tech stack details +- `business-domain.md` - Business context diff --git a/project-intelligence/living-notes.md b/project-intelligence/living-notes.md new file mode 100644 index 0000000..3cd479e --- /dev/null +++ b/project-intelligence/living-notes.md @@ -0,0 +1,64 @@ + + +# Living Notes + +**Purpose**: Development notes, TODOs, and temporary information. +**Last Updated**: 2026-02-27 + +## Quick Reference +- **Update When**: Adding dev notes, tracking issues +- **Audience**: Developers + +--- + +## Current Focus + +- Performance optimization (see PLAN_OPTIMISATION_PERFORMANCES.md) +- Reducing bundle size +- Image optimization + +--- + +## Development Notes + +### Service Layer +All business logic lives in `src/lib/services/`. API routes are thin wrappers. + +### API Error Handling +Use `AppError` class from `@/utils/errors`. Always include error code from `ERROR_CODES`. + +### Component Patterns +- UI components: `src/components/ui/` (Radix + Tailwind) +- Feature components: `src/components/*/` (by feature) +- Use `cva` for variant props +- Use `cn` from `@/lib/utils` for class merging + +### Types +- Komga types: `src/types/komga/` +- App types: `src/types/` + +### Database +- Prisma schema: `prisma/schema.prisma` +- MongoDB connection: `src/lib/prisma.ts` + +--- + +## Known Issues + +- Large libraries may be slow to load (pagination helps) +- Offline storage limited by device space + +--- + +## Future Ideas + +- [ ] Add more reader modes +- [ ] User collections/tags +- [ ] Reading statistics +- [ ] Better caching strategy + +--- + +## Related Files +- `technical-domain.md` - Code patterns +- `decisions-log.md` - Architecture decisions diff --git a/project-intelligence/navigation.md b/project-intelligence/navigation.md new file mode 100644 index 0000000..dbc7dad --- /dev/null +++ b/project-intelligence/navigation.md @@ -0,0 +1,23 @@ + + +# Project Intelligence + +Quick overview of project patterns and context files. + +## Quick Routes + +| File | Description | Priority | +|------|-------------|----------| +| [technical-domain.md](./technical-domain.md) | Tech stack, architecture, patterns | critical | +| [business-domain.md](./business-domain.md) | Business logic, domain model | high | +| [decisions-log.md](./decisions-log.md) | Architecture decisions | medium | +| [living-notes.md](./living-notes.md) | Development notes | low | +| [business-tech-bridge.md](./business-tech-bridge.md) | Business-technical mapping | medium | + +## All Files Complete | + +## Usage + +- **AI Agents**: Read technical-domain.md for code patterns +- **New Developers**: Start with technical-domain.md + business-domain.md +- **Architecture**: Check decisions-log.md for rationale diff --git a/project-intelligence/technical-domain.md b/project-intelligence/technical-domain.md new file mode 100644 index 0000000..190fc7c --- /dev/null +++ b/project-intelligence/technical-domain.md @@ -0,0 +1,154 @@ + + +# Technical Domain + +**Purpose**: Tech stack, architecture, development patterns for this project. +**Last Updated**: 2026-02-27 + +## Quick Reference +**Update Triggers**: Tech stack changes | New patterns | Architecture decisions +**Audience**: Developers, AI agents + +## Primary Stack +| Layer | Technology | Version | Rationale | +|-------|-----------|---------|-----------| +| Framework | Next.js | 15.5.9 | App Router, Server Components | +| Language | TypeScript | 5.3.3 | Type safety | +| Database | MongoDB | - | Flexible schema for media metadata | +| ORM | Prisma | 6.17.1 | Type-safe DB queries | +| Styling | Tailwind CSS | 3.4.1 | Utility-first | +| UI Library | Radix UI | - | Accessible components | +| Animation | Framer Motion | 12.x | Declarative animations | +| Auth | NextAuth | v5 | Session management | +| Validation | Zod | 3.22.4 | Schema validation | +| Logger | Pino | 10.x | Structured logging | + +## Project Structure +``` +src/ +├── app/ # Next.js App Router pages +├── components/ # React components (ui/, features/) +├── lib/ # Services, utils, config +│ └── services/ # Business logic (BookService, etc.) +├── hooks/ # Custom React hooks +├── types/ # TypeScript type definitions +├── utils/ # Helper functions +├── contexts/ # React contexts +├── constants/ # App constants +└── i18n/ # Internationalization +``` + +## Code Patterns +### API Endpoint +```typescript +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 logger from "@/lib/logger"; + +export async function GET(request: NextRequest, { params }: { params: Promise<{ bookId: string }> }) { + try { + const bookId: string = (await params).bookId; + const data = await BookService.getBook(bookId); + return NextResponse.json(data); + } catch (error) { + logger.error({ err: error }, "API Books - Erreur:"); + if (error instanceof AppError) { + const isNotFound = error.code === ERROR_CODES.BOOK.NOT_FOUND; + return NextResponse.json( + { error: { code: error.code, name: "Error", message: getErrorMessage(error.code) } }, + { status: isNotFound ? 404 : 500 } + ); + } + return NextResponse.json( + { error: { code: ERROR_CODES.BOOK.NOT_FOUND, name: "Error", message: "Internal error" } }, + { status: 500 } + ); + } +} +``` + +### Component with Variants +```typescript +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const buttonVariants = cva("inline-flex items-center justify-center...", { + variants: { + variant: { default: "...", destructive: "...", outline: "..." }, + size: { default: "...", sm: "...", lg: "..." }, + }, + defaultVariants: { variant: "default", size: "default" }, +}); + +export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef(({ className, variant, size, asChild, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ; +}); +Button.displayName = "Button"; + +export { Button, buttonVariants }; +``` + +### Feature Component +```typescript +import type { KomgaBook } from "@/types/komga"; + +interface HomeContentProps { + data: HomeData; +} + +export function HomeContent({ data }: HomeContentProps) { + return ( +
+ {data.ongoing && } +
+ ); +} +``` + +## Naming Conventions +| Type | Convention | Example | +|------|-----------|---------| +| Files | kebab-case | book-cover.tsx | +| Components | PascalCase | BookCover | +| Functions | camelCase | getBookById | +| Types | PascalCase | KomgaBook | +| Database | snake_case | read_progress | +| API Routes | kebab-case | /api/komga/books | + +## Code Standards +- TypeScript strict mode enabled +- Zod for request/response validation +- Prisma for all database queries (type-safe) +- Server Components by default, Client Components when needed +- Custom AppError class with error codes +- Structured logging with Pino +- Error responses: `{ error: { code, name, message } }` + +## Security Requirements +- Validate all user input with Zod +- Parameterized queries via Prisma (prevents SQL injection) +- Sanitize before rendering (React handles this) +- HTTPS only in production +- Auth via NextAuth v5 +- Role-based access control (admin, user) +- API routes protected with session checks + +## 📂 Codebase References +**API Routes**: `src/app/api/**/route.ts` - All API endpoints +**Services**: `src/lib/services/*.service.ts` - Business logic layer +**Components**: `src/components/ui/`, `src/components/*/` - UI and feature components +**Types**: `src/types/**` - TypeScript definitions +**Config**: package.json, tsconfig.json, prisma/schema.prisma + +## Related Files +- business-domain.md - Business logic and domain model +- decisions-log.md - Architecture decisions diff --git a/src/app/api/komga/books/[bookId]/route.ts b/src/app/api/komga/books/[bookId]/route.ts index d38644b..7d94332 100644 --- a/src/app/api/komga/books/[bookId]/route.ts +++ b/src/app/api/komga/books/[bookId]/route.ts @@ -7,6 +7,8 @@ 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 }> } diff --git a/src/app/api/komga/home/route.ts b/src/app/api/komga/home/route.ts index 48e3099..91d7e1b 100644 --- a/src/app/api/komga/home/route.ts +++ b/src/app/api/komga/home/route.ts @@ -5,6 +5,8 @@ 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(); diff --git a/src/app/api/komga/libraries/[libraryId]/series/route.ts b/src/app/api/komga/libraries/[libraryId]/series/route.ts index 2693188..46b280c 100644 --- a/src/app/api/komga/libraries/[libraryId]/series/route.ts +++ b/src/app/api/komga/libraries/[libraryId]/series/route.ts @@ -6,6 +6,8 @@ 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( diff --git a/src/app/api/komga/libraries/route.ts b/src/app/api/komga/libraries/route.ts index 6d3657d..c55670d 100644 --- a/src/app/api/komga/libraries/route.ts +++ b/src/app/api/komga/libraries/route.ts @@ -5,7 +5,8 @@ import { AppError } from "@/utils/errors"; import type { KomgaLibrary } from "@/types/komga"; import { getErrorMessage } from "@/utils/errors"; import logger from "@/lib/logger"; -export const dynamic = "force-dynamic"; + +// Cache handled in service via fetchFromApi options export async function GET() { try { diff --git a/src/app/api/komga/series/[seriesId]/books/route.ts b/src/app/api/komga/series/[seriesId]/books/route.ts index 36b016a..a5bb56e 100644 --- a/src/app/api/komga/series/[seriesId]/books/route.ts +++ b/src/app/api/komga/series/[seriesId]/books/route.ts @@ -6,6 +6,8 @@ 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( diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 49f5a7f..710aa0d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -93,7 +93,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo } if (librariesData.status === "fulfilled") { - libraries = librariesData.value; + libraries = librariesData.value || []; } if (favoritesData.status === "fulfilled") { diff --git a/src/lib/services/base-api.service.ts b/src/lib/services/base-api.service.ts index 393c659..199efcc 100644 --- a/src/lib/services/base-api.service.ts +++ b/src/lib/services/base-api.service.ts @@ -8,6 +8,8 @@ import logger from "@/lib/logger"; interface KomgaRequestInit extends RequestInit { isImage?: boolean; noJson?: boolean; + /** Next.js cache duration in seconds. Use false to disable cache, number for TTL */ + revalidate?: number | false; } interface KomgaUrlBuilder { @@ -90,7 +92,8 @@ export abstract class BaseApiService { } const isDebug = process.env.KOMGA_DEBUG === "true"; - const startTime = isDebug ? Date.now() : 0; + const isCacheDebug = process.env.CACHE_DEBUG === "true"; + const startTime = isDebug || isCacheDebug ? Date.now() : 0; if (isDebug) { logger.info( @@ -100,11 +103,23 @@ export abstract class BaseApiService { params, isImage: options.isImage, noJson: options.noJson, + revalidate: options.revalidate, }, "🔵 Komga Request" ); } + if (isCacheDebug && options.revalidate) { + logger.info( + { + url, + cache: "enabled", + ttl: options.revalidate, + }, + "💾 Cache enabled" + ); + } + // Timeout de 15 secondes pour éviter les blocages longs const timeoutMs = 15000; const controller = new AbortController(); @@ -122,6 +137,10 @@ export abstract class BaseApiService { connectTimeout: timeoutMs, bodyTimeout: timeoutMs, headersTimeout: timeoutMs, + // Next.js cache + next: options.revalidate !== undefined + ? { revalidate: options.revalidate } + : undefined, }); } catch (fetchError: any) { // Gestion spécifique des erreurs DNS @@ -139,6 +158,10 @@ export abstract class BaseApiService { // Force IPv4 si IPv6 pose problème // @ts-ignore family: 4, + // Next.js cache + next: options.revalidate !== undefined + ? { revalidate: options.revalidate } + : undefined, }); } else if (fetchError?.cause?.code === "UND_ERR_CONNECT_TIMEOUT") { // Retry automatique sur timeout de connexion (cold start) @@ -152,6 +175,10 @@ export abstract class BaseApiService { connectTimeout: timeoutMs, bodyTimeout: timeoutMs, headersTimeout: timeoutMs, + // Next.js cache + next: options.revalidate !== undefined + ? { revalidate: options.revalidate } + : undefined, }); } else { throw fetchError; @@ -160,8 +187,9 @@ export abstract class BaseApiService { clearTimeout(timeoutId); + const duration = Date.now() - startTime; + if (isDebug) { - const duration = Date.now() - startTime; logger.info( { url, @@ -173,6 +201,16 @@ export abstract class BaseApiService { ); } + // Log potential cache hit/miss based on response time + if (isCacheDebug && options.revalidate) { + // Fast response (< 50ms) is likely a cache hit + if (duration < 50) { + logger.info({ url, duration: `${duration}ms` }, "⚡ Cache HIT (fast response)"); + } else { + logger.info({ url, duration: `${duration}ms` }, "🔄 Cache MISS (slow response)"); + } + } + if (!response.ok) { if (isDebug) { logger.error( diff --git a/src/lib/services/book.service.ts b/src/lib/services/book.service.ts index 712bcb5..f7d6aa9 100644 --- a/src/lib/services/book.service.ts +++ b/src/lib/services/book.service.ts @@ -6,12 +6,22 @@ import { ERROR_CODES } from "../../constants/errorCodes"; import { AppError } from "../../utils/errors"; export class BookService extends BaseApiService { + private static readonly CACHE_TTL = 60; // 1 minute + static async getBook(bookId: string): Promise { try { // Récupération parallèle des détails du tome et des pages const [book, pages] = await Promise.all([ - this.fetchFromApi({ path: `books/${bookId}` }), - this.fetchFromApi<{ number: number }[]>({ path: `books/${bookId}/pages` }), + this.fetchFromApi( + { path: `books/${bookId}` }, + {}, + { revalidate: this.CACHE_TTL } + ), + this.fetchFromApi<{ number: number }[]>( + { path: `books/${bookId}/pages` }, + {}, + { revalidate: this.CACHE_TTL } + ), ]); return { @@ -44,7 +54,11 @@ export class BookService extends BaseApiService { static async getBookSeriesId(bookId: string): Promise { try { - const book = await this.fetchFromApi({ path: `books/${bookId}` }); + const book = await this.fetchFromApi( + { path: `books/${bookId}` }, + {}, + { revalidate: this.CACHE_TTL } + ); return book.seriesId; } catch (error) { throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {}, error); diff --git a/src/lib/services/home.service.ts b/src/lib/services/home.service.ts index c6eead2..4382cbe 100644 --- a/src/lib/services/home.service.ts +++ b/src/lib/services/home.service.ts @@ -8,53 +8,75 @@ import { AppError } from "../../utils/errors"; export type { HomeData }; export class HomeService extends BaseApiService { + private static readonly CACHE_TTL = 120; // 2 minutes + static async getHomeData(): Promise { try { const [ongoing, ongoingBooks, recentlyRead, onDeck, latestSeries] = await Promise.all([ - this.fetchFromApi>({ - path: "series", - params: { - read_status: "IN_PROGRESS", - sort: "readDate,desc", - page: "0", - size: "10", - media_status: "READY", + this.fetchFromApi>( + { + path: "series", + params: { + read_status: "IN_PROGRESS", + sort: "readDate,desc", + page: "0", + size: "10", + media_status: "READY", + }, }, - }), - this.fetchFromApi>({ - path: "books", - params: { - read_status: "IN_PROGRESS", - sort: "readProgress.readDate,desc", - page: "0", - size: "10", - media_status: "READY", + {}, + { revalidate: this.CACHE_TTL } + ), + this.fetchFromApi>( + { + path: "books", + params: { + read_status: "IN_PROGRESS", + sort: "readProgress.readDate,desc", + page: "0", + size: "10", + media_status: "READY", + }, }, - }), - this.fetchFromApi>({ - path: "books/latest", - params: { - page: "0", - size: "10", - media_status: "READY", + {}, + { revalidate: this.CACHE_TTL } + ), + this.fetchFromApi>( + { + path: "books/latest", + params: { + page: "0", + size: "10", + media_status: "READY", + }, }, - }), - this.fetchFromApi>({ - path: "books/ondeck", - params: { - page: "0", - size: "10", - media_status: "READY", + {}, + { revalidate: this.CACHE_TTL } + ), + this.fetchFromApi>( + { + path: "books/ondeck", + params: { + page: "0", + size: "10", + media_status: "READY", + }, }, - }), - this.fetchFromApi>({ - path: "series/latest", - params: { - page: "0", - size: "10", - media_status: "READY", + {}, + { revalidate: this.CACHE_TTL } + ), + this.fetchFromApi>( + { + path: "series/latest", + params: { + page: "0", + size: "10", + media_status: "READY", + }, }, - }), + {}, + { revalidate: this.CACHE_TTL } + ), ]); return { diff --git a/src/lib/services/library.service.ts b/src/lib/services/library.service.ts index ebe60db..3898e10 100644 --- a/src/lib/services/library.service.ts +++ b/src/lib/services/library.service.ts @@ -14,18 +14,28 @@ interface KomgaLibraryRaw { } export class LibraryService extends BaseApiService { + private static readonly CACHE_TTL = 300; // 5 minutes + static async getLibraries(): Promise { try { - const libraries = await this.fetchFromApi({ path: "libraries" }); + const libraries = await this.fetchFromApi( + { path: "libraries" }, + {}, + { revalidate: this.CACHE_TTL } + ); - // Enrich each library with book counts + // Enrich each library with book counts (parallel requests) const enrichedLibraries = await Promise.all( libraries.map(async (library) => { try { - const booksResponse = await this.fetchFromApi<{ totalElements: number }>({ - path: "books", - params: { library_id: library.id, size: "0" }, - }); + const booksResponse = await this.fetchFromApi<{ totalElements: number }>( + { + path: "books", + params: { library_id: library.id, size: "0" }, + }, + {}, + { revalidate: this.CACHE_TTL } + ); return { ...library, importLastModified: "", @@ -76,40 +86,19 @@ export class LibraryService extends BaseApiService { let condition: any; if (unreadOnly) { - // Utiliser allOf pour combiner libraryId avec anyOf pour UNREAD ou IN_PROGRESS condition = { allOf: [ - { - libraryId: { - operator: "is", - value: libraryId, - }, - }, + { libraryId: { operator: "is", value: libraryId } }, { anyOf: [ - { - readStatus: { - operator: "is", - value: "UNREAD", - }, - }, - { - readStatus: { - operator: "is", - value: "IN_PROGRESS", - }, - }, + { readStatus: { operator: "is", value: "UNREAD" } }, + { readStatus: { operator: "is", value: "IN_PROGRESS" } }, ], }, ], }; } else { - condition = { - libraryId: { - operator: "is", - value: libraryId, - }, - }; + condition = { libraryId: { operator: "is", value: libraryId } }; } const searchBody: { condition: any; fullTextSearch?: string } = { condition }; @@ -127,13 +116,10 @@ export class LibraryService extends BaseApiService { const response = await this.fetchFromApi>( { path: "series/list", params }, headers, - { - method: "POST", - body: JSON.stringify(searchBody), - } + { method: "POST", body: JSON.stringify(searchBody), revalidate: this.CACHE_TTL } ); - // Filtrer uniquement les séries supprimées côté client (léger) + // Filtrer uniquement les séries supprimées const filteredContent = response.content.filter((series) => !series.deleted); return { @@ -149,12 +135,9 @@ export class LibraryService extends BaseApiService { static async scanLibrary(libraryId: string, deep: boolean = false): Promise { try { await this.fetchFromApi( - { - path: `libraries/${libraryId}/scan`, - params: { deep: String(deep) }, - }, + { path: `libraries/${libraryId}/scan`, params: { deep: String(deep) } }, {}, - { method: "POST", noJson: true } + { method: "POST", noJson: true, revalidate: 0 } // bypass cache on mutations ); } catch (error) { throw new AppError(ERROR_CODES.LIBRARY.SCAN_ERROR, { libraryId }, error); diff --git a/src/lib/services/series.service.ts b/src/lib/services/series.service.ts index 2cf7b78..fe498c4 100644 --- a/src/lib/services/series.service.ts +++ b/src/lib/services/series.service.ts @@ -10,9 +10,15 @@ import type { UserPreferences } from "@/types/preferences"; import logger from "@/lib/logger"; export class SeriesService extends BaseApiService { + private static readonly CACHE_TTL = 120; // 2 minutes + static async getSeries(seriesId: string): Promise { try { - return this.fetchFromApi({ path: `series/${seriesId}` }); + return this.fetchFromApi( + { path: `series/${seriesId}` }, + {}, + { revalidate: this.CACHE_TTL } + ); } catch (error) { throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error); } @@ -81,6 +87,7 @@ export class SeriesService extends BaseApiService { { method: "POST", body: JSON.stringify(searchBody), + revalidate: this.CACHE_TTL, } );