feat: add caching debug logs and configurable max concurrent requests for Komga API to enhance performance monitoring
This commit is contained in:
6
ENV.md
6
ENV.md
@@ -17,6 +17,12 @@ NEXTAUTH_URL=https://ton-domaine.com
|
|||||||
# Admin User (optional - default password for julienfroidefond@gmail.com)
|
# Admin User (optional - default password for julienfroidefond@gmail.com)
|
||||||
ADMIN_DEFAULT_PASSWORD=Admin@2025
|
ADMIN_DEFAULT_PASSWORD=Admin@2025
|
||||||
|
|
||||||
|
# Cache Debug (optional - logs cache operations)
|
||||||
|
# CACHE_DEBUG=true
|
||||||
|
|
||||||
|
# Komga Request Queue (optional - max concurrent requests to Komga, default: 2)
|
||||||
|
# KOMGA_MAX_CONCURRENT_REQUESTS=5
|
||||||
|
|
||||||
# Node Environment
|
# Node Environment
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ services:
|
|||||||
- WATCHPACK_POLLING=true
|
- WATCHPACK_POLLING=true
|
||||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||||
- NEXTAUTH_URL=${NEXTAUTH_URL}
|
- NEXTAUTH_URL=${NEXTAUTH_URL}
|
||||||
|
- CACHE_DEBUG=true
|
||||||
|
- KOMGA_MAX_CONCURRENT_REQUESTS=5
|
||||||
command: sh -c "pnpm config set store-dir /app/.pnpm-store && pnpm install --frozen-lockfile && pnpm prisma generate && pnpm dev"
|
command: sh -c "pnpm config set store-dir /app/.pnpm-store && pnpm install --frozen-lockfile && pnpm prisma generate && pnpm dev"
|
||||||
|
|
||||||
mongodb:
|
mongodb:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ services:
|
|||||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||||
- NEXTAUTH_URL=${NEXTAUTH_URL}
|
- NEXTAUTH_URL=${NEXTAUTH_URL}
|
||||||
- AUTH_TRUST_HOST=true
|
- AUTH_TRUST_HOST=true
|
||||||
|
- KOMGA_MAX_CONCURRENT_REQUESTS=5
|
||||||
depends_on:
|
depends_on:
|
||||||
mongodb:
|
mongodb:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
317
docs/cache-debug.md
Normal file
317
docs/cache-debug.md
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
# Debug du Cache
|
||||||
|
|
||||||
|
Guide pour debugger le système de caching de StripStream.
|
||||||
|
|
||||||
|
## Activation des logs de cache
|
||||||
|
|
||||||
|
### Variable d'environnement
|
||||||
|
|
||||||
|
Activez les logs détaillés du cache serveur avec :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CACHE_DEBUG=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
#### Développement (docker-compose.dev.yml)
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- CACHE_DEBUG=true
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Production (.env)
|
||||||
|
```env
|
||||||
|
CACHE_DEBUG=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Format des logs
|
||||||
|
|
||||||
|
Les logs de cache apparaissent dans la console serveur avec le format suivant :
|
||||||
|
|
||||||
|
### Cache HIT (donnée valide)
|
||||||
|
```
|
||||||
|
[CACHE HIT] home-ongoing | HOME | 0.45ms
|
||||||
|
```
|
||||||
|
- ✅ Donnée trouvée en cache
|
||||||
|
- ✅ Donnée encore valide (pas expirée)
|
||||||
|
- ⚡ Retour immédiat (très rapide)
|
||||||
|
|
||||||
|
### Cache STALE (donnée expirée)
|
||||||
|
```
|
||||||
|
[CACHE STALE] home-ongoing | HOME | 0.52ms
|
||||||
|
```
|
||||||
|
- ✅ Donnée trouvée en cache
|
||||||
|
- ⚠️ Donnée expirée mais toujours retournée
|
||||||
|
- 🔄 Revalidation lancée en background
|
||||||
|
|
||||||
|
### Cache MISS (pas de donnée)
|
||||||
|
```
|
||||||
|
[CACHE MISS] home-ongoing | HOME
|
||||||
|
```
|
||||||
|
- ❌ Aucune donnée en cache
|
||||||
|
- 🌐 Fetch normal depuis Komga
|
||||||
|
- 💾 Mise en cache automatique
|
||||||
|
|
||||||
|
### Cache SET (mise en cache)
|
||||||
|
```
|
||||||
|
[CACHE SET] home-ongoing | HOME | 324.18ms
|
||||||
|
```
|
||||||
|
- 💾 Donnée mise en cache après fetch
|
||||||
|
- 📊 Temps total incluant le fetch Komga
|
||||||
|
- ✅ Prochaines requêtes seront rapides
|
||||||
|
|
||||||
|
### Cache REVALIDATE (revalidation background)
|
||||||
|
```
|
||||||
|
[CACHE REVALIDATE] home-ongoing | HOME | 287.45ms
|
||||||
|
```
|
||||||
|
- 🔄 Revalidation en background (après STALE)
|
||||||
|
- 🌐 Nouvelle donnée fetched depuis Komga
|
||||||
|
- 💾 Cache mis à jour pour les prochaines requêtes
|
||||||
|
|
||||||
|
### Erreur de revalidation
|
||||||
|
```
|
||||||
|
[CACHE REVALIDATE ERROR] home-ongoing: Error: ...
|
||||||
|
```
|
||||||
|
- ❌ Échec de la revalidation background
|
||||||
|
- ⚠️ Cache ancien conservé
|
||||||
|
- 🔄 Retry au prochain STALE
|
||||||
|
|
||||||
|
## Types de cache
|
||||||
|
|
||||||
|
Les logs affichent le type de TTL utilisé :
|
||||||
|
|
||||||
|
| Type | TTL | Usage |
|
||||||
|
|------|-----|-------|
|
||||||
|
| `DEFAULT` | 5 min | Données génériques |
|
||||||
|
| `HOME` | 10 min | Page d'accueil |
|
||||||
|
| `LIBRARIES` | 24h | Bibliothèques |
|
||||||
|
| `SERIES` | 5 min | Séries |
|
||||||
|
| `BOOKS` | 5 min | Livres |
|
||||||
|
| `IMAGES` | 7 jours | Images |
|
||||||
|
|
||||||
|
## Exemple de session complète
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Première requête (cache vide)
|
||||||
|
[CACHE MISS] home-ongoing | HOME
|
||||||
|
[CACHE SET] home-ongoing | HOME | 324.18ms
|
||||||
|
|
||||||
|
# Requête suivante (cache valide)
|
||||||
|
[CACHE HIT] home-ongoing | HOME | 0.45ms
|
||||||
|
|
||||||
|
# 10 minutes plus tard (cache expiré)
|
||||||
|
[CACHE STALE] home-ongoing | HOME | 0.52ms
|
||||||
|
[CACHE REVALIDATE] home-ongoing | HOME | 287.45ms
|
||||||
|
|
||||||
|
# Requête suivante (cache frais)
|
||||||
|
[CACHE HIT] home-ongoing | HOME | 0.43ms
|
||||||
|
```
|
||||||
|
|
||||||
|
## Outils complémentaires
|
||||||
|
|
||||||
|
### 1. DevTools du navigateur
|
||||||
|
|
||||||
|
#### Network Tab
|
||||||
|
- Temps de réponse < 50ms = probablement du cache serveur
|
||||||
|
- Headers `X-Cache` si configurés
|
||||||
|
- Onglet "Timing" pour détails
|
||||||
|
|
||||||
|
#### Application → Cache Storage
|
||||||
|
Inspectez le cache du Service Worker :
|
||||||
|
- `stripstream-cache-v1` : Ressources statiques
|
||||||
|
- `stripstream-images-v1` : Images (covers + pages)
|
||||||
|
|
||||||
|
Actions disponibles :
|
||||||
|
- ✅ Voir le contenu de chaque cache
|
||||||
|
- 🔍 Chercher une URL spécifique
|
||||||
|
- 🗑️ Supprimer des entrées
|
||||||
|
- 🧹 Vider complètement un cache
|
||||||
|
|
||||||
|
#### Application → Service Workers
|
||||||
|
- État du Service Worker
|
||||||
|
- "Unregister" pour le désactiver
|
||||||
|
- "Update" pour forcer une mise à jour
|
||||||
|
- Console pour voir les logs SW
|
||||||
|
|
||||||
|
### 2. API de monitoring
|
||||||
|
|
||||||
|
#### Taille du cache
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/api/komga/cache/size
|
||||||
|
```
|
||||||
|
Response :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sizeInBytes": 15728640,
|
||||||
|
"itemCount": 234
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mode actuel
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/api/komga/cache/mode
|
||||||
|
```
|
||||||
|
Response :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": "memory"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Vider le cache
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/komga/cache/clear
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Changer de mode
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/komga/cache/mode \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"mode": "file"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Mode fichier : Inspection du disque
|
||||||
|
|
||||||
|
Si vous utilisez le mode `file`, le cache est stocké sur disque :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Voir la structure du cache
|
||||||
|
ls -la .cache/
|
||||||
|
|
||||||
|
# Voir la taille totale
|
||||||
|
du -sh .cache/
|
||||||
|
|
||||||
|
# Compter les fichiers
|
||||||
|
find .cache/ -type f | wc -l
|
||||||
|
|
||||||
|
# Voir le contenu d'une entrée
|
||||||
|
cat .cache/user-id/home-ongoing.json | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemple de contenu :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"ongoing": [...],
|
||||||
|
"recentlyRead": [...],
|
||||||
|
"onDeck": [...]
|
||||||
|
},
|
||||||
|
"expiry": 1704067200000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Patterns de debug courants
|
||||||
|
|
||||||
|
### Identifier un problème de cache
|
||||||
|
|
||||||
|
**Symptôme** : Les données ne se rafraîchissent pas
|
||||||
|
```bash
|
||||||
|
# 1. Vérifier si STALE + REVALIDATE se produisent
|
||||||
|
CACHE_DEBUG=true
|
||||||
|
|
||||||
|
# 2. Observer les logs
|
||||||
|
[CACHE STALE] series-123 | SERIES | 0.5ms
|
||||||
|
[CACHE REVALIDATE ERROR] series-123: Network error
|
||||||
|
|
||||||
|
# 3. Problème identifié : Komga inaccessible
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution** : Vérifier la connectivité avec Komga
|
||||||
|
|
||||||
|
### Optimiser les performances
|
||||||
|
|
||||||
|
**Objectif** : Identifier les requêtes lentes
|
||||||
|
```bash
|
||||||
|
# Activer les logs
|
||||||
|
CACHE_DEBUG=true
|
||||||
|
|
||||||
|
# Observer les temps
|
||||||
|
[CACHE MISS] library-456-all-series | SERIES
|
||||||
|
[CACHE SET] library-456-all-series | SERIES | 2847.32ms # ⚠️ Très lent !
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution** :
|
||||||
|
- Vérifier la taille des bibliothèques
|
||||||
|
- Augmenter le TTL pour ces données
|
||||||
|
- Considérer la pagination
|
||||||
|
|
||||||
|
### Vérifier le mode de cache
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Logs après redémarrage
|
||||||
|
[CACHE MISS] home-ongoing | HOME # Mode memory : normal
|
||||||
|
[CACHE HIT] home-ongoing | HOME # Mode file : cache persisté
|
||||||
|
```
|
||||||
|
|
||||||
|
En mode `memory` : tous les caches sont vides au démarrage
|
||||||
|
En mode `file` : les caches survivent au redémarrage
|
||||||
|
|
||||||
|
## Performance attendue
|
||||||
|
|
||||||
|
### Temps de réponse normaux
|
||||||
|
|
||||||
|
| Scénario | Temps attendu | Log |
|
||||||
|
|----------|---------------|-----|
|
||||||
|
| Cache HIT | < 1ms | `[CACHE HIT] ... \| 0.45ms` |
|
||||||
|
| Cache STALE | < 1ms | `[CACHE STALE] ... \| 0.52ms` |
|
||||||
|
| Cache MISS (petit) | 50-200ms | `[CACHE SET] ... \| 124.18ms` |
|
||||||
|
| Cache MISS (gros) | 200-1000ms | `[CACHE SET] ... \| 847.32ms` |
|
||||||
|
| Revalidate (background) | Variable | `[CACHE REVALIDATE] ... \| 287.45ms` |
|
||||||
|
|
||||||
|
### Signaux d'alerte
|
||||||
|
|
||||||
|
⚠️ **Cache HIT > 10ms**
|
||||||
|
- Problème : Disque lent (mode file)
|
||||||
|
- Solution : Vérifier les I/O, passer en mode memory
|
||||||
|
|
||||||
|
⚠️ **Cache MISS > 2000ms**
|
||||||
|
- Problème : Komga très lent ou données énormes
|
||||||
|
- Solution : Vérifier Komga, optimiser la requête
|
||||||
|
|
||||||
|
⚠️ **REVALIDATE ERROR fréquents**
|
||||||
|
- Problème : Komga instable ou réseau
|
||||||
|
- Solution : Augmenter les timeouts, vérifier la connectivité
|
||||||
|
|
||||||
|
⚠️ **Trop de MISS successifs**
|
||||||
|
- Problème : Cache pas conservé ou TTL trop court
|
||||||
|
- Solution : Vérifier le mode, augmenter les TTL
|
||||||
|
|
||||||
|
## Désactiver les logs
|
||||||
|
|
||||||
|
Pour désactiver les logs de cache en production :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
CACHE_DEBUG=false
|
||||||
|
|
||||||
|
# ou simplement commenter/supprimer la ligne
|
||||||
|
# CACHE_DEBUG=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Les logs sont **automatiquement désactivés** si la variable n'est pas définie.
|
||||||
|
|
||||||
|
## Logs et performance
|
||||||
|
|
||||||
|
**Impact sur les performances** :
|
||||||
|
- Overhead : < 0.1ms par opération
|
||||||
|
- Pas d'écriture disque (juste console)
|
||||||
|
- Pas d'accumulation en mémoire
|
||||||
|
- Safe pour la production
|
||||||
|
|
||||||
|
**Recommandations** :
|
||||||
|
- ✅ Activé en développement
|
||||||
|
- ✅ Activé temporairement en production pour diagnostics
|
||||||
|
- ❌ Pas nécessaire en production normale
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Le système de logs de cache est conçu pour être :
|
||||||
|
- 🎯 **Simple** : Format clair et concis
|
||||||
|
- ⚡ **Rapide** : Impact négligeable sur les performances
|
||||||
|
- 🔧 **Utile** : Informations essentielles pour le debug
|
||||||
|
- 🔒 **Optionnel** : Désactivé par défaut
|
||||||
|
|
||||||
|
Pour la plupart des besoins de debug, les DevTools du navigateur suffisent.
|
||||||
|
Les logs serveur sont utiles pour comprendre le comportement du cache côté backend.
|
||||||
|
|
||||||
502
docs/caching.md
Normal file
502
docs/caching.md
Normal file
@@ -0,0 +1,502 @@
|
|||||||
|
# Système de Caching
|
||||||
|
|
||||||
|
Ce document décrit l'architecture et les stratégies de caching de StripStream.
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
Le système de caching est organisé en **3 couches indépendantes** avec des responsabilités clairement définies :
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ NAVIGATEUR │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Service Worker (Cache API) │ │
|
||||||
|
│ │ → Offline support │ │
|
||||||
|
│ │ → Images (covers + pages) │ │
|
||||||
|
│ └────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ SERVEUR NEXT.JS │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ ServerCacheService │ │
|
||||||
|
│ │ → Données API Komga │ │
|
||||||
|
│ │ → Stale-while-revalidate │ │
|
||||||
|
│ │ → Mode fichier ou mémoire │ │
|
||||||
|
│ └────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ SERVEUR KOMGA │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Couche 1 : Service Worker (Client)
|
||||||
|
|
||||||
|
### Fichier
|
||||||
|
`public/sw.js`
|
||||||
|
|
||||||
|
### Responsabilité
|
||||||
|
- Support offline de l'application
|
||||||
|
- Cache persistant des images (couvertures et pages de livres)
|
||||||
|
- Cache des ressources statiques Next.js
|
||||||
|
|
||||||
|
### Stratégies
|
||||||
|
|
||||||
|
#### Images : Cache-First
|
||||||
|
```javascript
|
||||||
|
// Pour toutes les images (covers + pages)
|
||||||
|
const isImageResource = (url) => {
|
||||||
|
return (
|
||||||
|
(url.includes("/api/v1/books/") &&
|
||||||
|
(url.includes("/pages") || url.includes("/thumbnail") || url.includes("/cover"))) ||
|
||||||
|
(url.includes("/api/komga/images/") &&
|
||||||
|
(url.includes("/series/") || url.includes("/books/")) &&
|
||||||
|
url.includes("/thumbnail"))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Comportement** :
|
||||||
|
1. Vérifier si l'image est dans le cache
|
||||||
|
2. Si oui → retourner depuis le cache
|
||||||
|
3. Si non → fetch depuis le réseau
|
||||||
|
4. Si succès → mettre en cache + retourner
|
||||||
|
5. Si échec → retourner 404
|
||||||
|
|
||||||
|
**Avantages** :
|
||||||
|
- Performance maximale (lecture instantanée depuis le cache)
|
||||||
|
- Fonctionne offline une fois les images chargées
|
||||||
|
- Économise la bande passante
|
||||||
|
|
||||||
|
#### Navigation et ressources statiques : Network-First
|
||||||
|
```javascript
|
||||||
|
// Pour les pages et ressources _next/static
|
||||||
|
event.respondWith(
|
||||||
|
fetch(request)
|
||||||
|
.then((response) => {
|
||||||
|
// Mise en cache si succès
|
||||||
|
if (response.ok && (isNextStaticResource || isNavigation)) {
|
||||||
|
cache.put(request, response.clone());
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(async () => {
|
||||||
|
// Fallback sur le cache si offline
|
||||||
|
const cachedResponse = await cache.match(request);
|
||||||
|
if (cachedResponse) return cachedResponse;
|
||||||
|
|
||||||
|
// Page offline si navigation
|
||||||
|
if (request.mode === "navigate") {
|
||||||
|
return cache.match("/offline.html");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Avantages** :
|
||||||
|
- Toujours la dernière version quand online
|
||||||
|
- Fallback offline si nécessaire
|
||||||
|
- Navigation fluide même sans connexion
|
||||||
|
|
||||||
|
### Caches
|
||||||
|
|
||||||
|
| Cache | Usage | Stratégie | Taille |
|
||||||
|
|-------|-------|-----------|--------|
|
||||||
|
| `stripstream-cache-v1` | Ressources statiques + pages | Network-First | ~5 MB |
|
||||||
|
| `stripstream-images-v1` | Images (covers + pages) | Cache-First | Illimité |
|
||||||
|
|
||||||
|
### Nettoyage
|
||||||
|
- Automatique lors de l'activation du Service Worker
|
||||||
|
- Suppression des anciennes versions de cache
|
||||||
|
- Pas d'expiration (contrôlé par l'utilisateur via les paramètres du navigateur)
|
||||||
|
|
||||||
|
## Couche 2 : ServerCacheService (Serveur)
|
||||||
|
|
||||||
|
### Fichier
|
||||||
|
`src/lib/services/server-cache.service.ts`
|
||||||
|
|
||||||
|
### Responsabilité
|
||||||
|
- Cache des réponses API Komga côté serveur
|
||||||
|
- Optimisation des temps de réponse
|
||||||
|
- Réduction de la charge sur Komga
|
||||||
|
|
||||||
|
### Stratégie : Stale-While-Revalidate
|
||||||
|
|
||||||
|
Cette stratégie est **la clé de la performance** de l'application.
|
||||||
|
|
||||||
|
#### Principe
|
||||||
|
```
|
||||||
|
Requête → Cache existe ?
|
||||||
|
├─ Non → Fetch normal + mise en cache
|
||||||
|
└─ Oui → Cache valide ?
|
||||||
|
├─ Oui → Retourne immédiatement
|
||||||
|
└─ Non → Retourne le cache expiré (stale)
|
||||||
|
ET revalide en background
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Implémentation
|
||||||
|
```typescript
|
||||||
|
async getOrSet<T>(
|
||||||
|
key: string,
|
||||||
|
fetcher: () => Promise<T>,
|
||||||
|
type: keyof typeof ServerCacheService.DEFAULT_TTL = "DEFAULT"
|
||||||
|
): Promise<T> {
|
||||||
|
const cacheKey = `${user.id}-${key}`;
|
||||||
|
const cachedResult = this.getStale(cacheKey);
|
||||||
|
|
||||||
|
if (cachedResult !== null) {
|
||||||
|
const { data, isStale } = cachedResult;
|
||||||
|
|
||||||
|
// Si le cache est expiré, revalider en background
|
||||||
|
if (isStale) {
|
||||||
|
this.revalidateInBackground(cacheKey, fetcher, type, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as T; // Retour immédiat
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pas de cache, fetch normal
|
||||||
|
const data = await fetcher();
|
||||||
|
this.set(cacheKey, data, type);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Avantages
|
||||||
|
✅ **Temps de réponse constant** : Le cache expiré est retourné instantanément
|
||||||
|
✅ **Données fraîches** : Revalidation en background pour la prochaine requête
|
||||||
|
✅ **Pas de délai** : L'utilisateur ne subit jamais l'attente de revalidation
|
||||||
|
✅ **Résilience** : Même si Komga est lent, l'app reste rapide
|
||||||
|
|
||||||
|
#### Inconvénients
|
||||||
|
⚠️ Les données peuvent être légèrement obsolètes (jusqu'au prochain refresh)
|
||||||
|
⚠️ Nécessite un cache initialisé (première requête toujours lente)
|
||||||
|
|
||||||
|
### Modes de stockage
|
||||||
|
|
||||||
|
L'utilisateur peut choisir entre deux modes :
|
||||||
|
|
||||||
|
#### Mode Mémoire (par défaut)
|
||||||
|
```typescript
|
||||||
|
cacheMode: "memory"
|
||||||
|
```
|
||||||
|
- Cache stocké en RAM
|
||||||
|
- **Performances** : Très rapide (lecture < 1ms)
|
||||||
|
- **Persistance** : Perdu au redémarrage du serveur
|
||||||
|
- **Capacité** : Limitée par la RAM disponible
|
||||||
|
- **Idéal pour** : Développement, faible charge
|
||||||
|
|
||||||
|
#### Mode Fichier
|
||||||
|
```typescript
|
||||||
|
cacheMode: "file"
|
||||||
|
```
|
||||||
|
- Cache stocké sur disque (`.cache/`)
|
||||||
|
- **Performances** : Rapide (lecture 5-10ms)
|
||||||
|
- **Persistance** : Survit aux redémarrages
|
||||||
|
- **Capacité** : Limitée par l'espace disque
|
||||||
|
- **Idéal pour** : Production, haute charge
|
||||||
|
|
||||||
|
### Time-To-Live (TTL)
|
||||||
|
|
||||||
|
Chaque type de données a un TTL configuré :
|
||||||
|
|
||||||
|
| Type | TTL par défaut | Justification |
|
||||||
|
|------|----------------|---------------|
|
||||||
|
| `DEFAULT` | 5 minutes | Données génériques |
|
||||||
|
| `HOME` | 10 minutes | Page d'accueil (données agrégées) |
|
||||||
|
| `LIBRARIES` | 24 heures | Bibliothèques (rarement modifiées) |
|
||||||
|
| `SERIES` | 5 minutes | Séries (métadonnées + progression) |
|
||||||
|
| `BOOKS` | 5 minutes | Livres (métadonnées + progression) |
|
||||||
|
| `IMAGES` | 7 jours | Images (immuables) |
|
||||||
|
|
||||||
|
#### Configuration personnalisée
|
||||||
|
|
||||||
|
Les TTL peuvent être personnalisés par l'utilisateur via la base de données :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Modèle Prisma : TTLConfig
|
||||||
|
{
|
||||||
|
defaultTTL: 5 * 60 * 1000,
|
||||||
|
homeTTL: 10 * 60 * 1000,
|
||||||
|
librariesTTL: 24 * 60 * 60 * 1000,
|
||||||
|
seriesTTL: 5 * 60 * 1000,
|
||||||
|
booksTTL: 5 * 60 * 1000,
|
||||||
|
imagesTTL: 7 * 24 * 60 * 60 * 1000,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Isolation par utilisateur
|
||||||
|
|
||||||
|
Chaque utilisateur a son propre cache :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const cacheKey = `${user.id}-${key}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Avantages** :
|
||||||
|
- Pas de collision entre utilisateurs
|
||||||
|
- Progression de lecture individuelle
|
||||||
|
- Préférences personnalisées
|
||||||
|
|
||||||
|
### Invalidation du cache
|
||||||
|
|
||||||
|
Le cache peut être invalidé :
|
||||||
|
|
||||||
|
#### Manuellement
|
||||||
|
```typescript
|
||||||
|
await cacheService.delete(key); // Une clé
|
||||||
|
await cacheService.deleteAll(prefix); // Toutes les clés avec préfixe
|
||||||
|
await cacheService.clear(); // Tout le cache
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Automatiquement
|
||||||
|
- Lors d'une mise à jour de progression
|
||||||
|
- Lors d'un changement de favoris
|
||||||
|
- Lors de la suppression d'une série
|
||||||
|
|
||||||
|
#### API
|
||||||
|
```
|
||||||
|
DELETE /api/komga/cache/clear // Vider tout le cache
|
||||||
|
DELETE /api/komga/home // Invalider le cache home
|
||||||
|
```
|
||||||
|
|
||||||
|
## Couche 3 : Cache HTTP (Navigateur)
|
||||||
|
|
||||||
|
### Responsabilité
|
||||||
|
- Cache basique géré par le navigateur
|
||||||
|
- Headers HTTP standard
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
#### Next.js ISR (Incremental Static Regeneration)
|
||||||
|
```typescript
|
||||||
|
export const revalidate = 60; // Revalidation toutes les 60 secondes
|
||||||
|
```
|
||||||
|
|
||||||
|
Utilisé uniquement pour les routes avec rendu statique.
|
||||||
|
|
||||||
|
#### Headers explicites (désactivé)
|
||||||
|
|
||||||
|
Les headers HTTP explicites ont été **supprimés** car :
|
||||||
|
- Le ServerCacheService gère déjà le caching efficacement
|
||||||
|
- Évite la confusion entre plusieurs couches de cache
|
||||||
|
- Simplifie le debugging
|
||||||
|
|
||||||
|
Avant (supprimé) :
|
||||||
|
```typescript
|
||||||
|
NextResponse.json(data, {
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=120'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Maintenant :
|
||||||
|
```typescript
|
||||||
|
NextResponse.json(data); // Pas de headers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flow de données complet
|
||||||
|
|
||||||
|
Exemple : Chargement de la page d'accueil
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Utilisateur → GET /
|
||||||
|
↓
|
||||||
|
2. Next.js → HomeService.getHomeData()
|
||||||
|
↓
|
||||||
|
3. HomeService → ServerCacheService.getOrSet("home-ongoing", ...)
|
||||||
|
↓
|
||||||
|
4. ServerCacheService
|
||||||
|
├─ Cache valide ? → Retourne immédiatement
|
||||||
|
├─ Cache expiré ? → Retourne cache + revalide en background
|
||||||
|
└─ Pas de cache ? → Fetch Komga + mise en cache
|
||||||
|
↓
|
||||||
|
5. Response → Client
|
||||||
|
↓
|
||||||
|
6. Images → Service Worker (Cache-First)
|
||||||
|
├─ En cache ? → Lecture instantanée
|
||||||
|
└─ Pas en cache ? → Fetch + mise en cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### Temps de réponse typiques
|
||||||
|
|
||||||
|
| Scénario | Temps | Détails |
|
||||||
|
|----------|-------|---------|
|
||||||
|
| Cache ServerCache valide + SW | ~50ms | Optimal |
|
||||||
|
| Cache ServerCache expiré + SW | ~50ms | Revalidation en background |
|
||||||
|
| Pas de cache ServerCache + SW | ~200-500ms | Première requête |
|
||||||
|
| Cache SW uniquement | ~10ms | Images seulement |
|
||||||
|
| Tout à froid | ~500-1000ms | Pire cas |
|
||||||
|
|
||||||
|
## Cas d'usage
|
||||||
|
|
||||||
|
### 1. Première visite
|
||||||
|
```
|
||||||
|
User → App → Komga (tous les caches vides)
|
||||||
|
Temps : ~500-1000ms
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Visite suivante (online)
|
||||||
|
```
|
||||||
|
User → ServerCache (valide) → Images SW
|
||||||
|
Temps : ~50ms
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Cache expiré (online)
|
||||||
|
```
|
||||||
|
User → ServerCache (stale) → Retour immédiat
|
||||||
|
↓
|
||||||
|
Revalidation background → Mise à jour cache
|
||||||
|
Temps ressenti : ~50ms (aucun délai)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Mode offline
|
||||||
|
```
|
||||||
|
User → Service Worker cache uniquement
|
||||||
|
Fonctionnalités :
|
||||||
|
✅ Navigation entre pages déjà visitées
|
||||||
|
✅ Consultation des images déjà vues
|
||||||
|
❌ Nouvelles données (nécessite connexion)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring et debug
|
||||||
|
|
||||||
|
### Logs de cache (recommandé pour le dev)
|
||||||
|
|
||||||
|
Activez les logs détaillés du cache serveur :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dans docker-compose.dev.yml ou .env
|
||||||
|
CACHE_DEBUG=true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Format des logs** :
|
||||||
|
```
|
||||||
|
[CACHE HIT] home-ongoing | HOME | 0.45ms # Cache valide
|
||||||
|
[CACHE STALE] home-ongoing | HOME | 0.52ms # Cache expiré (retourné + revalidation)
|
||||||
|
[CACHE MISS] home-ongoing | HOME # Pas de cache
|
||||||
|
[CACHE SET] home-ongoing | HOME | 324.18ms # Mise en cache
|
||||||
|
[CACHE REVALIDATE] home-ongoing | HOME | 287ms # Revalidation background
|
||||||
|
```
|
||||||
|
|
||||||
|
📖 **Documentation complète** : [docs/cache-debug.md](./cache-debug.md)
|
||||||
|
|
||||||
|
### API de monitoring
|
||||||
|
|
||||||
|
#### Taille du cache serveur
|
||||||
|
```bash
|
||||||
|
GET /api/komga/cache/size
|
||||||
|
Response: { sizeInBytes: 15728640, itemCount: 234 }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mode de cache actuel
|
||||||
|
```bash
|
||||||
|
GET /api/komga/cache/mode
|
||||||
|
Response: { mode: "memory" }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Changer le mode
|
||||||
|
```bash
|
||||||
|
POST /api/komga/cache/mode
|
||||||
|
Body: { mode: "file" }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Vider le cache
|
||||||
|
```bash
|
||||||
|
POST /api/komga/cache/clear
|
||||||
|
```
|
||||||
|
|
||||||
|
### DevTools du navigateur
|
||||||
|
|
||||||
|
#### Network Tab
|
||||||
|
- Temps de réponse < 50ms = cache serveur
|
||||||
|
- Headers `X-Cache` si configurés
|
||||||
|
- Onglet "Timing" pour détails
|
||||||
|
|
||||||
|
#### Application → Cache Storage
|
||||||
|
Inspecter le Service Worker :
|
||||||
|
- `stripstream-cache-v1` : Ressources statiques
|
||||||
|
- `stripstream-images-v1` : Images
|
||||||
|
|
||||||
|
Actions disponibles :
|
||||||
|
- Voir le contenu
|
||||||
|
- Supprimer des entrées
|
||||||
|
- Vider complètement
|
||||||
|
|
||||||
|
#### Application → Service Workers
|
||||||
|
- État du Service Worker
|
||||||
|
- "Unregister" pour le désactiver
|
||||||
|
- "Update" pour forcer une mise à jour
|
||||||
|
|
||||||
|
## Optimisations futures possibles
|
||||||
|
|
||||||
|
### 1. Cache Redis (optionnel)
|
||||||
|
- Pour un déploiement multi-instances
|
||||||
|
- Cache partagé entre plusieurs serveurs
|
||||||
|
- TTL natif Redis
|
||||||
|
|
||||||
|
### 2. Compression
|
||||||
|
- Compresser les données en cache (Brotli/Gzip)
|
||||||
|
- Économie d'espace disque/mémoire
|
||||||
|
- Trade-off CPU vs espace
|
||||||
|
|
||||||
|
### 3. Prefetching intelligent
|
||||||
|
- Précharger les séries en cours de lecture
|
||||||
|
- Précharger les pages suivantes dans le reader
|
||||||
|
- Basé sur l'historique utilisateur
|
||||||
|
|
||||||
|
### 4. Cache Analytics
|
||||||
|
- Ratio hit/miss
|
||||||
|
- Temps de réponse moyens
|
||||||
|
- Identification des données les plus consultées
|
||||||
|
|
||||||
|
## Bonnes pratiques
|
||||||
|
|
||||||
|
### Pour les développeurs
|
||||||
|
|
||||||
|
✅ **Utiliser BaseApiService.fetchWithCache()**
|
||||||
|
```typescript
|
||||||
|
await this.fetchWithCache<T>(
|
||||||
|
"cache-key",
|
||||||
|
async () => this.fetchFromApi(...),
|
||||||
|
"HOME" // Type de TTL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Invalider le cache après modification**
|
||||||
|
```typescript
|
||||||
|
await HomeService.invalidateHomeCache();
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Choisir le bon TTL**
|
||||||
|
- Court (1-5 min) : Données qui changent souvent
|
||||||
|
- Moyen (10-30 min) : Données agrégées
|
||||||
|
- Long (24h+) : Données quasi-statiques
|
||||||
|
|
||||||
|
❌ **Ne pas cacher les mutations**
|
||||||
|
Les POST/PUT/DELETE ne doivent jamais être cachés
|
||||||
|
|
||||||
|
❌ **Ne pas oublier l'isolation utilisateur**
|
||||||
|
Toujours préfixer avec `userId` pour les données personnelles
|
||||||
|
|
||||||
|
### Pour les utilisateurs
|
||||||
|
|
||||||
|
- **Mode mémoire** : Plus rapide, mais cache perdu au redémarrage
|
||||||
|
- **Mode fichier** : Persistant, idéal pour production
|
||||||
|
- **Vider le cache** : En cas de problème d'affichage
|
||||||
|
- **Offline** : Consulter les pages déjà visitées
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Le système de caching de StripStream est conçu pour :
|
||||||
|
|
||||||
|
🎯 **Performance** : Temps de réponse constants grâce au stale-while-revalidate
|
||||||
|
🔒 **Fiabilité** : Fonctionne même si Komga est lent ou inaccessible
|
||||||
|
💾 **Flexibilité** : Mode mémoire ou fichier selon les besoins
|
||||||
|
🚀 **Offline-first** : Support complet du mode hors ligne
|
||||||
|
🧹 **Simplicité** : 3 couches bien définies, pas de redondance
|
||||||
|
|
||||||
|
Le système est maintenu simple avec des responsabilités claires pour chaque couche, facilitant la maintenance et l'évolution future.
|
||||||
|
|
||||||
@@ -65,7 +65,6 @@ model Preferences {
|
|||||||
showThumbnails Boolean @default(true)
|
showThumbnails Boolean @default(true)
|
||||||
cacheMode String @default("memory") // "memory" | "file"
|
cacheMode String @default("memory") // "memory" | "file"
|
||||||
showOnlyUnread Boolean @default(false)
|
showOnlyUnread Boolean @default(false)
|
||||||
debug Boolean @default(false)
|
|
||||||
displayMode Json @default("{\"compact\": false, \"itemsPerPage\": 20}")
|
displayMode Json @default("{\"compact\": false, \"itemsPerPage\": 20}")
|
||||||
background Json @default("{\"type\": \"default\"}")
|
background Json @default("{\"type\": \"default\"}")
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|||||||
127
public/sw.js
127
public/sw.js
@@ -1,6 +1,5 @@
|
|||||||
const CACHE_NAME = "stripstream-cache-v1";
|
const CACHE_NAME = "stripstream-cache-v1";
|
||||||
const BOOKS_CACHE_NAME = "stripstream-books";
|
const IMAGES_CACHE_NAME = "stripstream-images-v1";
|
||||||
const COVERS_CACHE_NAME = "stripstream-covers";
|
|
||||||
const OFFLINE_PAGE = "/offline.html";
|
const OFFLINE_PAGE = "/offline.html";
|
||||||
|
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
@@ -16,8 +15,7 @@ self.addEventListener("install", (event) => {
|
|||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
Promise.all([
|
Promise.all([
|
||||||
caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)),
|
caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)),
|
||||||
caches.open(BOOKS_CACHE_NAME),
|
caches.open(IMAGES_CACHE_NAME),
|
||||||
caches.open(COVERS_CACHE_NAME),
|
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -28,9 +26,7 @@ self.addEventListener("activate", (event) => {
|
|||||||
caches.keys().then((cacheNames) => {
|
caches.keys().then((cacheNames) => {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
cacheNames
|
cacheNames
|
||||||
.filter(
|
.filter((name) => name !== CACHE_NAME && name !== IMAGES_CACHE_NAME)
|
||||||
(name) => name !== CACHE_NAME && name !== BOOKS_CACHE_NAME && name !== COVERS_CACHE_NAME
|
|
||||||
)
|
|
||||||
.map((name) => caches.delete(name))
|
.map((name) => caches.delete(name))
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -53,45 +49,31 @@ const isNextStaticResource = (url) => {
|
|||||||
return url.includes("/_next/static") && !isWebpackResource(url);
|
return url.includes("/_next/static") && !isWebpackResource(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fonction pour vérifier si c'est une ressource de livre
|
// Fonction pour vérifier si c'est une image (couvertures ou pages de livres)
|
||||||
const isBookResource = (url) => {
|
const isImageResource = (url) => {
|
||||||
return url.includes("/api/v1/books/") && (url.includes("/pages") || url.includes("/thumbnail"));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fonction pour vérifier si c'est une image de couverture
|
|
||||||
const isCoverImage = (url) => {
|
|
||||||
const urlParams = new URLSearchParams(url.split("?")[1]);
|
|
||||||
const originalUrl = urlParams.get("url") || url;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
originalUrl.includes("/api/komga/images/") &&
|
(url.includes("/api/v1/books/") && (url.includes("/pages") || url.includes("/thumbnail") || url.includes("/cover"))) ||
|
||||||
(originalUrl.includes("/series/") || originalUrl.includes("/books/")) &&
|
(url.includes("/api/komga/images/") && (url.includes("/series/") || url.includes("/books/")) && url.includes("/thumbnail"))
|
||||||
originalUrl.includes("/thumbnail")
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stratégie de cache pour les images de couverture
|
// Stratégie Cache-First pour les images
|
||||||
const coverCacheStrategy = async (request) => {
|
const imageCacheStrategy = async (request) => {
|
||||||
const urlParams = new URLSearchParams(request.url.split("?")[1]);
|
const cache = await caches.open(IMAGES_CACHE_NAME);
|
||||||
const originalUrl = urlParams.get("url") || request.url;
|
const cachedResponse = await cache.match(request);
|
||||||
const originalRequest = new Request(originalUrl, request);
|
|
||||||
|
|
||||||
const cache = await caches.open(COVERS_CACHE_NAME);
|
|
||||||
const cachedResponse = await cache.match(originalRequest);
|
|
||||||
|
|
||||||
if (cachedResponse) {
|
if (cachedResponse) {
|
||||||
return cachedResponse;
|
return cachedResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(originalRequest);
|
const response = await fetch(request);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
await cache.put(originalRequest, response.clone());
|
await cache.put(request, response.clone());
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
throw new Error("Network response was not ok");
|
// Si 404, retourner une réponse vide sans throw (pas d'erreur console)
|
||||||
} catch (error) {
|
if (response.status === 404) {
|
||||||
console.error("Error fetching cover image", error);
|
|
||||||
return new Response("", {
|
return new Response("", {
|
||||||
status: 404,
|
status: 404,
|
||||||
statusText: "Not Found",
|
statusText: "Not Found",
|
||||||
@@ -100,26 +82,20 @@ const coverCacheStrategy = async (request) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
// Pour les autres erreurs, throw
|
||||||
|
throw new Error(`Network response error: ${response.status}`);
|
||||||
// Écouteur pour les messages du client
|
} catch (error) {
|
||||||
self.addEventListener("message", (event) => {
|
// Erreurs réseau (offline, timeout, etc.)
|
||||||
if (event.data && event.data.type === "CACHE_COVER") {
|
console.warn("Image fetch failed:", error);
|
||||||
const { url } = event.data;
|
return new Response("", {
|
||||||
if (url && isCoverImage(url)) {
|
status: 503,
|
||||||
fetch(url)
|
statusText: "Service Unavailable",
|
||||||
.then(async (response) => {
|
headers: {
|
||||||
if (response.ok) {
|
"Content-Type": "text/plain",
|
||||||
const cache = await caches.open(COVERS_CACHE_NAME);
|
},
|
||||||
await cache.put(url, response.clone());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Ignorer les erreurs de mise en cache
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener("fetch", (event) => {
|
self.addEventListener("fetch", (event) => {
|
||||||
// Ignorer les requêtes non GET
|
// Ignorer les requêtes non GET
|
||||||
@@ -128,54 +104,13 @@ self.addEventListener("fetch", (event) => {
|
|||||||
// Ignorer les ressources webpack
|
// Ignorer les ressources webpack
|
||||||
if (isWebpackResource(event.request.url)) return;
|
if (isWebpackResource(event.request.url)) return;
|
||||||
|
|
||||||
// Gérer les requêtes d'images de couverture
|
// Gérer les images avec Cache-First
|
||||||
if (event.request.url.includes("/api/v1/books/") && event.request.url.includes("/cover")) {
|
if (isImageResource(event.request.url)) {
|
||||||
event.respondWith(
|
event.respondWith(imageCacheStrategy(event.request));
|
||||||
caches.match(event.request).then((response) => {
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetch(event.request)
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
return new Response(null, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseToCache = response.clone();
|
|
||||||
caches.open(CACHE_NAME).then((cache) => {
|
|
||||||
cache.put(event.request, responseToCache);
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
return new Response(null, { status: 404 });
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pour les images de couverture
|
|
||||||
if (isCoverImage(event.request.url)) {
|
|
||||||
event.respondWith(coverCacheStrategy(event.request));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pour les ressources de livre
|
// Pour les ressources statiques de Next.js et les autres requêtes : Network-First
|
||||||
if (isBookResource(event.request.url)) {
|
|
||||||
event.respondWith(
|
|
||||||
caches.match(event.request).then((response) => {
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
return fetch(event.request);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pour les ressources statiques de Next.js et les autres requêtes
|
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
fetch(event.request)
|
fetch(event.request)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
import type { NextRequest} from "next/server";
|
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
import type { RequestTiming } from "@/lib/services/debug.service";
|
|
||||||
import { DebugService } from "@/lib/services/debug.service";
|
|
||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
|
||||||
import { getErrorMessage } from "@/utils/errors";
|
|
||||||
import { AppError } from "@/utils/errors";
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
const logs: RequestTiming[] = await DebugService.getRequestLogs();
|
|
||||||
return NextResponse.json(logs);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de la récupération des logs:", error);
|
|
||||||
if (error instanceof AppError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: error.code,
|
|
||||||
name: "Debug fetch error",
|
|
||||||
message: getErrorMessage(error.code),
|
|
||||||
} as AppError,
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: ERROR_CODES.DEBUG.FETCH_ERROR,
|
|
||||||
name: "Debug fetch error",
|
|
||||||
message: getErrorMessage(ERROR_CODES.DEBUG.FETCH_ERROR),
|
|
||||||
} as AppError,
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const timing: RequestTiming = await request.json();
|
|
||||||
await DebugService.logRequest(timing);
|
|
||||||
return NextResponse.json({
|
|
||||||
message: "✅ Log enregistré avec succès",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de l'enregistrement du log:", error);
|
|
||||||
if (error instanceof AppError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: error.code,
|
|
||||||
name: "Debug save error",
|
|
||||||
message: getErrorMessage(error.code),
|
|
||||||
} as AppError,
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: ERROR_CODES.DEBUG.SAVE_ERROR,
|
|
||||||
name: "Debug save error",
|
|
||||||
message: getErrorMessage(ERROR_CODES.DEBUG.SAVE_ERROR),
|
|
||||||
} as AppError,
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE() {
|
|
||||||
try {
|
|
||||||
await DebugService.clearLogs();
|
|
||||||
return NextResponse.json({
|
|
||||||
message: "🧹 Logs supprimés avec succès",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de la suppression des logs:", error);
|
|
||||||
if (error instanceof AppError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: error.code,
|
|
||||||
name: "Debug clear error",
|
|
||||||
message: getErrorMessage(error.code),
|
|
||||||
} as AppError,
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
code: ERROR_CODES.DEBUG.CLEAR_ERROR,
|
|
||||||
name: "Debug clear error",
|
|
||||||
message: getErrorMessage(ERROR_CODES.DEBUG.CLEAR_ERROR),
|
|
||||||
} as AppError,
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,11 +8,7 @@ export const revalidate = 60;
|
|||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const data = await HomeService.getHomeData();
|
const data = await HomeService.getHomeData();
|
||||||
return NextResponse.json(data, {
|
return NextResponse.json(data);
|
||||||
headers: {
|
|
||||||
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=120'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("API Home - Erreur:", error);
|
console.error("API Home - Erreur:", error);
|
||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { BookService } from "@/lib/services/book.service";
|
|||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||||
import { AppError } from "@/utils/errors";
|
import { AppError } from "@/utils/errors";
|
||||||
import { getErrorMessage } from "@/utils/errors";
|
import { getErrorMessage } from "@/utils/errors";
|
||||||
|
import { findHttpStatus } from "@/utils/image-errors";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -18,6 +19,26 @@ export async function GET(
|
|||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la récupération de la page du livre:", error);
|
console.error("Erreur lors de la récupération de la page du livre:", error);
|
||||||
|
|
||||||
|
// Chercher un status HTTP 404 dans la chaîne d'erreurs
|
||||||
|
const httpStatus = findHttpStatus(error);
|
||||||
|
|
||||||
|
if (httpStatus === 404) {
|
||||||
|
const { bookId, pageNumber } = await params;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`📷 Page ${pageNumber} not found for book: ${bookId}`);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
code: ERROR_CODES.IMAGE.FETCH_ERROR,
|
||||||
|
name: "Image not found",
|
||||||
|
message: "Image not found",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { BookService } from "@/lib/services/book.service";
|
|||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||||
import { AppError } from "@/utils/errors";
|
import { AppError } from "@/utils/errors";
|
||||||
import { getErrorMessage } from "@/utils/errors";
|
import { getErrorMessage } from "@/utils/errors";
|
||||||
|
import { findHttpStatus } from "@/utils/image-errors";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -32,6 +33,27 @@ export async function GET(
|
|||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la récupération de la miniature de la page:", error);
|
console.error("Erreur lors de la récupération de la miniature de la page:", error);
|
||||||
|
|
||||||
|
// Chercher un status HTTP 404 dans la chaîne d'erreurs
|
||||||
|
const httpStatus = findHttpStatus(error);
|
||||||
|
|
||||||
|
if (httpStatus === 404) {
|
||||||
|
const { bookId, pageNumber: pageNumberParam } = await params;
|
||||||
|
const pageNumber: number = parseInt(pageNumberParam);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`📷 Page ${pageNumber} thumbnail not found for book: ${bookId}`);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
code: ERROR_CODES.IMAGE.FETCH_ERROR,
|
||||||
|
name: "Image not found",
|
||||||
|
message: "Image not found",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { BookService } from "@/lib/services/book.service";
|
|||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||||
import { AppError } from "@/utils/errors";
|
import { AppError } from "@/utils/errors";
|
||||||
import { getErrorMessage } from "@/utils/errors";
|
import { getErrorMessage } from "@/utils/errors";
|
||||||
|
import { findHttpStatus } from "@/utils/image-errors";
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
@@ -16,6 +17,26 @@ export async function GET(
|
|||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la récupération de la miniature du livre:", error);
|
console.error("Erreur lors de la récupération de la miniature du livre:", error);
|
||||||
|
|
||||||
|
// Chercher un status HTTP 404 dans la chaîne d'erreurs
|
||||||
|
const httpStatus = findHttpStatus(error);
|
||||||
|
|
||||||
|
if (httpStatus === 404) {
|
||||||
|
const bookId: string = (await params).bookId;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`📷 Thumbnail not found for book: ${bookId}`);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
code: ERROR_CODES.IMAGE.FETCH_ERROR,
|
||||||
|
name: "Image not found",
|
||||||
|
message: "Image not found",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { SeriesService } from "@/lib/services/series.service";
|
|||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||||
import { AppError } from "@/utils/errors";
|
import { AppError } from "@/utils/errors";
|
||||||
import { getErrorMessage } from "@/utils/errors";
|
import { getErrorMessage } from "@/utils/errors";
|
||||||
|
import { findHttpStatus } from "@/utils/image-errors";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -18,6 +19,26 @@ export async function GET(
|
|||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la récupération de la couverture de la série:", error);
|
console.error("Erreur lors de la récupération de la couverture de la série:", error);
|
||||||
|
|
||||||
|
// Chercher un status HTTP 404 dans la chaîne d'erreurs
|
||||||
|
const httpStatus = findHttpStatus(error);
|
||||||
|
|
||||||
|
if (httpStatus === 404) {
|
||||||
|
const seriesId: string = (await params).seriesId;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`📷 First page image not found for series: ${seriesId}`);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
code: ERROR_CODES.IMAGE.FETCH_ERROR,
|
||||||
|
name: "Image not found",
|
||||||
|
message: "Image not found",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { SeriesService } from "@/lib/services/series.service";
|
|||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||||
import { AppError } from "@/utils/errors";
|
import { AppError } from "@/utils/errors";
|
||||||
import { getErrorMessage } from "@/utils/errors";
|
import { getErrorMessage } from "@/utils/errors";
|
||||||
|
import { findHttpStatus } from "@/utils/image-errors";
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
@@ -15,6 +16,26 @@ export async function GET(
|
|||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la récupération de la miniature de la série:", error);
|
console.error("Erreur lors de la récupération de la miniature de la série:", error);
|
||||||
|
|
||||||
|
// Chercher un status HTTP 404 dans la chaîne d'erreurs
|
||||||
|
const httpStatus = findHttpStatus(error);
|
||||||
|
|
||||||
|
if (httpStatus === 404) {
|
||||||
|
const seriesId: string = (await params).seriesId;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`📷 Image not found for series: ${seriesId}`);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
code: ERROR_CODES.IMAGE.FETCH_ERROR,
|
||||||
|
name: "Image not found",
|
||||||
|
message: "Image not found",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { ClientBookPage } from "@/components/reader/ClientBookPage";
|
import { ClientBookPage } from "@/components/reader/ClientBookPage";
|
||||||
import { BookSkeleton } from "@/components/skeletons/BookSkeleton";
|
import { BookSkeleton } from "@/components/skeletons/BookSkeleton";
|
||||||
import { withPageTiming } from "@/lib/hoc/withPageTiming";
|
|
||||||
|
|
||||||
async function BookPage({ params }: { params: { bookId: string } }) {
|
export default async function BookPage({ params }: { params: Promise<{ bookId: string }> }) {
|
||||||
const { bookId } = await params;
|
const { bookId } = await params;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -12,5 +11,3 @@ async function BookPage({ params }: { params: { bookId: string } }) {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withPageTiming("BookPage", BookPage);
|
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import { DownloadManager } from "@/components/downloads/DownloadManager";
|
import { DownloadManager } from "@/components/downloads/DownloadManager";
|
||||||
import { withPageTiming } from "@/lib/hoc/withPageTiming";
|
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
function DownloadsPage() {
|
export default function DownloadsPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DownloadManager />
|
<DownloadManager />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withPageTiming("DownloadsPage", DownloadsPage);
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { LoginContent } from "./LoginContent";
|
import { LoginContent } from "./LoginContent";
|
||||||
import { withPageTiming } from "@/lib/hoc/withPageTiming";
|
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
@@ -13,7 +12,6 @@ export const metadata: Metadata = {
|
|||||||
description: "Connectez-vous à votre compte StripStream",
|
description: "Connectez-vous à votre compte StripStream",
|
||||||
};
|
};
|
||||||
|
|
||||||
async function LoginPage({ searchParams }: PageProps) {
|
export default async function LoginPage({ searchParams }: PageProps) {
|
||||||
return <LoginContent searchParams={await searchParams} />;
|
return <LoginContent searchParams={await searchParams} />;
|
||||||
}
|
}
|
||||||
export default withPageTiming("LoginPage", LoginPage);
|
|
||||||
|
|||||||
@@ -1,281 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import {
|
|
||||||
X,
|
|
||||||
Database,
|
|
||||||
Minimize2,
|
|
||||||
Maximize2,
|
|
||||||
Clock,
|
|
||||||
CircleDot,
|
|
||||||
Layout,
|
|
||||||
RefreshCw,
|
|
||||||
Globe,
|
|
||||||
Filter,
|
|
||||||
Calendar,
|
|
||||||
} from "lucide-react";
|
|
||||||
import type { RequestTiming } from "@/types/debug";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useDebug } from "@/contexts/DebugContext";
|
|
||||||
|
|
||||||
function formatTime(timestamp: string) {
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
return date.toLocaleTimeString("fr-FR", {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDuration(duration: number) {
|
|
||||||
return Math.round(duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilterType = "all" | "current-page" | "api" | "cache" | "mongodb" | "page-render";
|
|
||||||
|
|
||||||
export function DebugInfo() {
|
|
||||||
const { logs, setLogs, clearLogs, isRefreshing, setIsRefreshing } = useDebug();
|
|
||||||
const [isMinimized, setIsMinimized] = useState(false);
|
|
||||||
const [filter, setFilter] = useState<FilterType>("all");
|
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
|
||||||
const pathname = usePathname();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const fetchLogs = async () => {
|
|
||||||
try {
|
|
||||||
setIsRefreshing(true);
|
|
||||||
const response = await fetch("/api/debug");
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
setLogs(data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de la récupération des logs:", error);
|
|
||||||
} finally {
|
|
||||||
setIsRefreshing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fonction pour déterminer si une requête appartient à la page courante
|
|
||||||
const isCurrentPageRequest = (log: RequestTiming): boolean => {
|
|
||||||
if (log.pageRender) {
|
|
||||||
return log.pageRender.page === pathname;
|
|
||||||
}
|
|
||||||
// Pour les requêtes API, on considère qu'elles appartiennent à la page courante
|
|
||||||
// si elles ont été faites récemment (dans les 30 dernières secondes)
|
|
||||||
const logTime = new Date(log.timestamp).getTime();
|
|
||||||
const now = Date.now();
|
|
||||||
return now - logTime < 30000; // 30 secondes
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filtrer les logs selon le filtre sélectionné
|
|
||||||
const filteredLogs = logs.filter((log) => {
|
|
||||||
switch (filter) {
|
|
||||||
case "current-page":
|
|
||||||
return isCurrentPageRequest(log);
|
|
||||||
case "api":
|
|
||||||
return !log.fromCache && !log.mongoAccess && !log.pageRender;
|
|
||||||
case "cache":
|
|
||||||
return log.fromCache;
|
|
||||||
case "mongodb":
|
|
||||||
return log.mongoAccess;
|
|
||||||
case "page-render":
|
|
||||||
return log.pageRender;
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortedLogs = [...filteredLogs].reverse();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`fixed bottom-4 right-4 bg-zinc-900/90 backdrop-blur-md border border-zinc-700 rounded-lg shadow-lg p-4 text-zinc-100 z-50 ${
|
|
||||||
isMinimized ? "w-auto" : "w-[800px] max-h-[50vh] overflow-auto"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-4 sticky top-0 bg-zinc-900/90 backdrop-blur-md pb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h2 className="font-bold text-lg">{t("debug.title")}</h2>
|
|
||||||
{!isMinimized && (
|
|
||||||
<span className="text-xs text-zinc-400">
|
|
||||||
{sortedLogs.length} {t("debug.entries", { count: sortedLogs.length })}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{!isMinimized && (
|
|
||||||
<button
|
|
||||||
onClick={() => setShowFilters(!showFilters)}
|
|
||||||
className={`hover:bg-zinc-700/80 hover:backdrop-blur-md rounded-full p-1.5 ${showFilters ? "bg-zinc-700/80 backdrop-blur-md" : ""}`}
|
|
||||||
aria-label="Filtres"
|
|
||||||
>
|
|
||||||
<Filter className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={fetchLogs}
|
|
||||||
className="hover:bg-zinc-700/80 hover:backdrop-blur-md rounded-full p-1.5"
|
|
||||||
aria-label={t("debug.actions.refresh")}
|
|
||||||
disabled={isRefreshing}
|
|
||||||
>
|
|
||||||
<RefreshCw className={`h-5 w-5 ${isRefreshing ? "animate-spin" : ""}`} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsMinimized(!isMinimized)}
|
|
||||||
className="hover:bg-zinc-700/80 hover:backdrop-blur-md rounded-full p-1.5"
|
|
||||||
aria-label={t(isMinimized ? "debug.actions.maximize" : "debug.actions.minimize")}
|
|
||||||
>
|
|
||||||
{isMinimized ? <Maximize2 className="h-5 w-5" /> : <Minimize2 className="h-5 w-5" />}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={clearLogs}
|
|
||||||
className="hover:bg-zinc-700/80 hover:backdrop-blur-md rounded-full p-1.5"
|
|
||||||
aria-label={t("debug.actions.clear")}
|
|
||||||
>
|
|
||||||
<X className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isMinimized && showFilters && (
|
|
||||||
<div className="mb-4 p-3 bg-zinc-800/80 backdrop-blur-md rounded-lg">
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{[
|
|
||||||
{ key: "all", label: "Toutes", icon: Calendar },
|
|
||||||
{ key: "current-page", label: "Page courante", icon: Layout },
|
|
||||||
{ key: "api", label: "API", icon: Globe },
|
|
||||||
{ key: "cache", label: "Cache", icon: Database },
|
|
||||||
{ key: "mongodb", label: "MongoDB", icon: CircleDot },
|
|
||||||
{ key: "page-render", label: "Rendu", icon: Layout },
|
|
||||||
].map(({ key, label, icon: Icon }) => (
|
|
||||||
<button
|
|
||||||
key={key}
|
|
||||||
onClick={() => setFilter(key as FilterType)}
|
|
||||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-full text-xs transition-colors ${
|
|
||||||
filter === key
|
|
||||||
? "bg-blue-600 text-white"
|
|
||||||
: "bg-zinc-700 text-zinc-300 hover:bg-zinc-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon className="h-3 w-3" />
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isMinimized && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{sortedLogs.length === 0 ? (
|
|
||||||
<p className="text-sm opacity-75">{t("debug.noRequests")}</p>
|
|
||||||
) : (
|
|
||||||
sortedLogs.map((log, index) => {
|
|
||||||
const isCurrentPage = isCurrentPageRequest(log);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`text-sm space-y-1.5 p-2 rounded border-l-2 ${
|
|
||||||
isCurrentPage
|
|
||||||
? "bg-blue-900/20 border-blue-500"
|
|
||||||
: "bg-zinc-800 border-zinc-700"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
||||||
{log.fromCache && (
|
|
||||||
<div
|
|
||||||
title={t("debug.tooltips.cache", { type: log.cacheType || "DEFAULT" })}
|
|
||||||
className="flex-shrink-0"
|
|
||||||
>
|
|
||||||
<Database className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{log.mongoAccess && (
|
|
||||||
<div
|
|
||||||
title={t("debug.tooltips.mongodb", {
|
|
||||||
operation: log.mongoAccess.operation,
|
|
||||||
})}
|
|
||||||
className="flex-shrink-0"
|
|
||||||
>
|
|
||||||
<CircleDot className="h-4 w-4 text-blue-400" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{log.pageRender && (
|
|
||||||
<div
|
|
||||||
title={t("debug.tooltips.pageRender", { page: log.pageRender.page })}
|
|
||||||
className="flex-shrink-0"
|
|
||||||
>
|
|
||||||
<Layout className="h-4 w-4 text-purple-400" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!log.fromCache && !log.mongoAccess && !log.pageRender && (
|
|
||||||
<div title={t("debug.tooltips.apiCall")} className="flex-shrink-0">
|
|
||||||
<Globe className="h-4 w-4 text-rose-400" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<span className="font-medium truncate" title={log.url}>
|
|
||||||
{log.url}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 flex-shrink-0">
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-1 text-zinc-400"
|
|
||||||
title={t("debug.tooltips.logTime")}
|
|
||||||
>
|
|
||||||
<Clock className="h-3 w-3" />
|
|
||||||
<span>{formatTime(log.timestamp)}</span>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
log.pageRender
|
|
||||||
? "text-purple-400"
|
|
||||||
: log.mongoAccess
|
|
||||||
? "text-blue-400"
|
|
||||||
: log.fromCache
|
|
||||||
? "text-emerald-400"
|
|
||||||
: "text-rose-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{formatDuration(log.duration)}ms
|
|
||||||
</span>
|
|
||||||
{log.mongoAccess && (
|
|
||||||
<span className="text-blue-400" title={t("debug.tooltips.mongoAccess")}>
|
|
||||||
+{formatDuration(log.mongoAccess.duration)}ms
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="h-1.5 bg-zinc-700 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className={`h-full ${
|
|
||||||
log.pageRender
|
|
||||||
? "bg-purple-500"
|
|
||||||
: log.fromCache
|
|
||||||
? "bg-emerald-500"
|
|
||||||
: "bg-rose-500"
|
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
width: `${Math.min((log.duration / 1000) * 100, 100)}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{log.mongoAccess && (
|
|
||||||
<div
|
|
||||||
className="h-full bg-blue-500"
|
|
||||||
style={{
|
|
||||||
width: `${Math.min((log.mongoAccess.duration / 1000) * 100, 100)}%`,
|
|
||||||
marginTop: "-6px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { usePreferences } from "@/contexts/PreferencesContext";
|
|
||||||
import { DebugInfo } from "./DebugInfo";
|
|
||||||
|
|
||||||
export function DebugWrapper() {
|
|
||||||
const { preferences } = usePreferences();
|
|
||||||
if (!preferences.debug) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <DebugInfo />;
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,6 @@ import { Toaster } from "@/components/ui/toaster";
|
|||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { registerServiceWorker } from "@/lib/registerSW";
|
import { registerServiceWorker } from "@/lib/registerSW";
|
||||||
import { NetworkStatus } from "../ui/NetworkStatus";
|
import { NetworkStatus } from "../ui/NetworkStatus";
|
||||||
import { DebugWrapper } from "@/components/debug/DebugWrapper";
|
|
||||||
import { DebugProvider } from "@/contexts/DebugContext";
|
|
||||||
import { usePreferences } from "@/contexts/PreferencesContext";
|
import { usePreferences } from "@/contexts/PreferencesContext";
|
||||||
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
||||||
|
|
||||||
@@ -94,7 +92,6 @@ export default function ClientLayout({ children, initialLibraries = [], initialF
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
<DebugProvider>
|
|
||||||
<div className="relative min-h-screen bg-background" style={backgroundStyle}>
|
<div className="relative min-h-screen bg-background" style={backgroundStyle}>
|
||||||
{!isPublicRoute && <Header onToggleSidebar={handleToggleSidebar} />}
|
{!isPublicRoute && <Header onToggleSidebar={handleToggleSidebar} />}
|
||||||
{!isPublicRoute && (
|
{!isPublicRoute && (
|
||||||
@@ -110,9 +107,7 @@ export default function ClientLayout({ children, initialLibraries = [], initialF
|
|||||||
<InstallPWA />
|
<InstallPWA />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<NetworkStatus />
|
<NetworkStatus />
|
||||||
<DebugWrapper />
|
|
||||||
</div>
|
</div>
|
||||||
</DebugProvider>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,34 +75,6 @@ export function DisplaySettings() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<Label htmlFor="debug-mode">{t("settings.display.debugMode.label")}</Label>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t("settings.display.debugMode.description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id="debug-mode"
|
|
||||||
checked={preferences.debug}
|
|
||||||
onCheckedChange={async (checked) => {
|
|
||||||
try {
|
|
||||||
await updatePreferences({ debug: checked });
|
|
||||||
toast({
|
|
||||||
title: t("settings.title"),
|
|
||||||
description: t("settings.komga.messages.configSaved"),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur détaillée:", error);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("settings.error.title"),
|
|
||||||
description: t("settings.error.message"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -87,11 +87,6 @@ export const ERROR_CODES = {
|
|||||||
INVALID_TOKEN: "MIDDLEWARE_INVALID_TOKEN",
|
INVALID_TOKEN: "MIDDLEWARE_INVALID_TOKEN",
|
||||||
INVALID_SESSION: "MIDDLEWARE_INVALID_SESSION",
|
INVALID_SESSION: "MIDDLEWARE_INVALID_SESSION",
|
||||||
},
|
},
|
||||||
DEBUG: {
|
|
||||||
FETCH_ERROR: "DEBUG_FETCH_ERROR",
|
|
||||||
SAVE_ERROR: "DEBUG_SAVE_ERROR",
|
|
||||||
CLEAR_ERROR: "DEBUG_CLEAR_ERROR",
|
|
||||||
},
|
|
||||||
CLIENT: {
|
CLIENT: {
|
||||||
FETCH_ERROR: "CLIENT_FETCH_ERROR",
|
FETCH_ERROR: "CLIENT_FETCH_ERROR",
|
||||||
NETWORK_ERROR: "CLIENT_NETWORK_ERROR",
|
NETWORK_ERROR: "CLIENT_NETWORK_ERROR",
|
||||||
|
|||||||
@@ -85,11 +85,6 @@ export const ERROR_MESSAGES: Record<string, string> = {
|
|||||||
[ERROR_CODES.CONFIG.TTL_SAVE_ERROR]: "⏱️ Error saving TTL configuration",
|
[ERROR_CODES.CONFIG.TTL_SAVE_ERROR]: "⏱️ Error saving TTL configuration",
|
||||||
[ERROR_CODES.CONFIG.TTL_FETCH_ERROR]: "⏱️ Error fetching TTL configuration",
|
[ERROR_CODES.CONFIG.TTL_FETCH_ERROR]: "⏱️ Error fetching TTL configuration",
|
||||||
|
|
||||||
// Debug
|
|
||||||
[ERROR_CODES.DEBUG.FETCH_ERROR]: "🔍 Error fetching logs",
|
|
||||||
[ERROR_CODES.DEBUG.SAVE_ERROR]: "💾 Error saving log",
|
|
||||||
[ERROR_CODES.DEBUG.CLEAR_ERROR]: "🧹 Error clearing logs",
|
|
||||||
|
|
||||||
// Client
|
// Client
|
||||||
[ERROR_CODES.CLIENT.FETCH_ERROR]: "🌐 Error during request",
|
[ERROR_CODES.CLIENT.FETCH_ERROR]: "🌐 Error during request",
|
||||||
[ERROR_CODES.CLIENT.NETWORK_ERROR]: "📡 Network connection error",
|
[ERROR_CODES.CLIENT.NETWORK_ERROR]: "📡 Network connection error",
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { createContext, useContext, useState, useEffect, ReactNode } from "react";
|
|
||||||
import type { RequestTiming } from "@/types/debug";
|
|
||||||
import { usePreferences } from "./PreferencesContext";
|
|
||||||
|
|
||||||
interface DebugContextType {
|
|
||||||
logs: RequestTiming[];
|
|
||||||
setLogs: (logs: RequestTiming[]) => void;
|
|
||||||
addLog: (log: RequestTiming) => void;
|
|
||||||
clearLogs: () => void;
|
|
||||||
isRefreshing: boolean;
|
|
||||||
setIsRefreshing: (refreshing: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DebugContext = createContext<DebugContextType | undefined>(undefined);
|
|
||||||
|
|
||||||
interface DebugProviderProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DebugProvider({ children }: DebugProviderProps) {
|
|
||||||
const [logs, setLogs] = useState<RequestTiming[]>([]);
|
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
||||||
const { preferences } = usePreferences();
|
|
||||||
|
|
||||||
const addLog = (log: RequestTiming) => {
|
|
||||||
setLogs(prevLogs => {
|
|
||||||
// Éviter les doublons basés sur l'URL et le timestamp
|
|
||||||
const exists = prevLogs.some(existingLog =>
|
|
||||||
existingLog.url === log.url && existingLog.timestamp === log.timestamp
|
|
||||||
);
|
|
||||||
if (exists) return prevLogs;
|
|
||||||
|
|
||||||
return [...prevLogs, log].sort((a, b) =>
|
|
||||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearLogs = async () => {
|
|
||||||
try {
|
|
||||||
// Vider le fichier côté serveur
|
|
||||||
await fetch("/api/debug", { method: "DELETE" });
|
|
||||||
// Vider le state côté client
|
|
||||||
setLogs([]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de la suppression des logs:", error);
|
|
||||||
// Même en cas d'erreur, vider le state côté client
|
|
||||||
setLogs([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Charger les logs au montage du provider et les rafraîchir périodiquement
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchLogs = async () => {
|
|
||||||
try {
|
|
||||||
// Ne pas faire de requête si le debug n'est pas activé
|
|
||||||
if (!preferences.debug) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsRefreshing(true);
|
|
||||||
const debugResponse = await fetch("/api/debug");
|
|
||||||
if (debugResponse.ok) {
|
|
||||||
const serverLogs = await debugResponse.json();
|
|
||||||
setLogs(serverLogs);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de la récupération des logs:", error);
|
|
||||||
} finally {
|
|
||||||
setIsRefreshing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchLogs();
|
|
||||||
|
|
||||||
// Rafraîchir toutes les 10 secondes (moins fréquent pour éviter les conflits)
|
|
||||||
const interval = setInterval(fetchLogs, 10000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [preferences.debug]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebugContext.Provider
|
|
||||||
value={{
|
|
||||||
logs,
|
|
||||||
setLogs,
|
|
||||||
addLog,
|
|
||||||
clearLogs,
|
|
||||||
isRefreshing,
|
|
||||||
setIsRefreshing
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</DebugContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDebug() {
|
|
||||||
const context = useContext(DebugContext);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error("useDebug must be used within a DebugProvider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
@@ -77,10 +77,6 @@
|
|||||||
"unreadFilter": {
|
"unreadFilter": {
|
||||||
"label": "Default Unread Filter",
|
"label": "Default Unread Filter",
|
||||||
"description": "Show only unread series by default"
|
"description": "Show only unread series by default"
|
||||||
},
|
|
||||||
"debugMode": {
|
|
||||||
"label": "Debug mode",
|
|
||||||
"description": "Show debug information in the interface"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"background": {
|
"background": {
|
||||||
@@ -388,10 +384,6 @@
|
|||||||
"MIDDLEWARE_INVALID_TOKEN": "Invalid authentication token",
|
"MIDDLEWARE_INVALID_TOKEN": "Invalid authentication token",
|
||||||
"MIDDLEWARE_INVALID_SESSION": "Invalid session",
|
"MIDDLEWARE_INVALID_SESSION": "Invalid session",
|
||||||
|
|
||||||
"DEBUG_FETCH_ERROR": "Error fetching debug logs",
|
|
||||||
"DEBUG_SAVE_ERROR": "Error saving debug logs",
|
|
||||||
"DEBUG_CLEAR_ERROR": "Error clearing debug logs",
|
|
||||||
|
|
||||||
"CLIENT_FETCH_ERROR": "Error fetching data",
|
"CLIENT_FETCH_ERROR": "Error fetching data",
|
||||||
"CLIENT_NETWORK_ERROR": "Network error",
|
"CLIENT_NETWORK_ERROR": "Network error",
|
||||||
"CLIENT_REQUEST_FAILED": "Request failed",
|
"CLIENT_REQUEST_FAILED": "Request failed",
|
||||||
@@ -434,25 +426,5 @@
|
|||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"retry": "Retry"
|
"retry": "Retry"
|
||||||
},
|
|
||||||
"debug": {
|
|
||||||
"title": "DEBUG",
|
|
||||||
"entries": "entry",
|
|
||||||
"entries_plural": "entries",
|
|
||||||
"noRequests": "No recorded requests",
|
|
||||||
"actions": {
|
|
||||||
"refresh": "Refresh logs",
|
|
||||||
"maximize": "Maximize",
|
|
||||||
"minimize": "Minimize",
|
|
||||||
"clear": "Clear logs"
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"cache": "Cache: {{type}}",
|
|
||||||
"mongodb": "MongoDB: {{operation}}",
|
|
||||||
"pageRender": "Page Render: {{page}}",
|
|
||||||
"apiCall": "API Call",
|
|
||||||
"logTime": "Log time",
|
|
||||||
"mongoAccess": "MongoDB access time"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,10 +77,6 @@
|
|||||||
"unreadFilter": {
|
"unreadFilter": {
|
||||||
"label": "Filtre \"À lire\" par défaut",
|
"label": "Filtre \"À lire\" par défaut",
|
||||||
"description": "Afficher uniquement les séries non lues par défaut"
|
"description": "Afficher uniquement les séries non lues par défaut"
|
||||||
},
|
|
||||||
"debugMode": {
|
|
||||||
"label": "Mode debug",
|
|
||||||
"description": "Afficher les informations de debug dans l'interface"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"background": {
|
"background": {
|
||||||
@@ -386,10 +382,6 @@
|
|||||||
"MIDDLEWARE_INVALID_TOKEN": "Jeton d'authentification invalide",
|
"MIDDLEWARE_INVALID_TOKEN": "Jeton d'authentification invalide",
|
||||||
"MIDDLEWARE_INVALID_SESSION": "Session invalide",
|
"MIDDLEWARE_INVALID_SESSION": "Session invalide",
|
||||||
|
|
||||||
"DEBUG_FETCH_ERROR": "Erreur lors de la récupération des logs de debug",
|
|
||||||
"DEBUG_SAVE_ERROR": "Erreur lors de la sauvegarde des logs de debug",
|
|
||||||
"DEBUG_CLEAR_ERROR": "Erreur lors de la suppression des logs de debug",
|
|
||||||
|
|
||||||
"CLIENT_FETCH_ERROR": "Erreur lors de la récupération des données",
|
"CLIENT_FETCH_ERROR": "Erreur lors de la récupération des données",
|
||||||
"CLIENT_NETWORK_ERROR": "Erreur réseau",
|
"CLIENT_NETWORK_ERROR": "Erreur réseau",
|
||||||
"CLIENT_REQUEST_FAILED": "La requête a échoué",
|
"CLIENT_REQUEST_FAILED": "La requête a échoué",
|
||||||
@@ -436,25 +428,5 @@
|
|||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"retry": "Réessayer"
|
"retry": "Réessayer"
|
||||||
},
|
|
||||||
"debug": {
|
|
||||||
"title": "DEBUG",
|
|
||||||
"entries": "entrée",
|
|
||||||
"entries_plural": "entrées",
|
|
||||||
"noRequests": "Aucune requête enregistrée",
|
|
||||||
"actions": {
|
|
||||||
"refresh": "Rafraîchir les logs",
|
|
||||||
"maximize": "Agrandir",
|
|
||||||
"minimize": "Minimiser",
|
|
||||||
"clear": "Effacer les logs"
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"cache": "Cache: {{type}}",
|
|
||||||
"mongodb": "MongoDB: {{operation}}",
|
|
||||||
"pageRender": "Page Render: {{page}}",
|
|
||||||
"apiCall": "API Call",
|
|
||||||
"logTime": "Heure du log",
|
|
||||||
"mongoAccess": "Temps d'accès MongoDB"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { ReactElement } from "react";
|
|
||||||
import { DebugService } from "@/lib/services/debug.service";
|
|
||||||
|
|
||||||
type PageComponent = (props: any) => Promise<ReactElement> | ReactElement;
|
|
||||||
|
|
||||||
export function withPageTiming(pageName: string, Component: PageComponent) {
|
|
||||||
return async function PageWithTiming(props: any) {
|
|
||||||
const start = performance.now();
|
|
||||||
const result = await Promise.resolve(Component(props));
|
|
||||||
const duration = performance.now() - start;
|
|
||||||
|
|
||||||
// Ensure params is awaited before using it
|
|
||||||
const params = props.params ? await Promise.resolve(props.params) : {};
|
|
||||||
|
|
||||||
// Only log if debug is enabled and user is authenticated
|
|
||||||
try {
|
|
||||||
await DebugService.logPageRender(pageName + JSON.stringify(params), duration);
|
|
||||||
} catch {
|
|
||||||
// Silently fail if user is not authenticated or debug is disabled
|
|
||||||
// This prevents errors on public pages like /login
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import { ERROR_CODES } from "../../constants/errorCodes";
|
|||||||
import { AppError } from "../../utils/errors";
|
import { AppError } from "../../utils/errors";
|
||||||
import type { KomgaConfig } from "@/types/komga";
|
import type { KomgaConfig } from "@/types/komga";
|
||||||
import type { ServerCacheService } from "./server-cache.service";
|
import type { ServerCacheService } from "./server-cache.service";
|
||||||
import { DebugService } from "./debug.service";
|
|
||||||
import { RequestMonitorService } from "./request-monitor.service";
|
import { RequestMonitorService } from "./request-monitor.service";
|
||||||
import { RequestQueueService } from "./request-queue.service";
|
import { RequestQueueService } from "./request-queue.service";
|
||||||
|
|
||||||
@@ -109,8 +108,6 @@ export abstract class BaseApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
// Timeout de 60 secondes au lieu de 10 par défaut
|
// Timeout de 60 secondes au lieu de 10 par défaut
|
||||||
const timeoutMs = 60000;
|
const timeoutMs = 60000;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@@ -149,19 +146,27 @@ export abstract class BaseApiService {
|
|||||||
family: 4,
|
family: 4,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Retry automatique sur timeout de connexion (cold start)
|
||||||
|
if (fetchError?.cause?.code === 'UND_ERR_CONNECT_TIMEOUT') {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`⏱️ Connection timeout for ${url}. Retrying once (cold start)...`);
|
||||||
|
|
||||||
|
return await fetch(url, {
|
||||||
|
headers,
|
||||||
|
...options,
|
||||||
|
signal: controller.signal,
|
||||||
|
// @ts-ignore - undici-specific options
|
||||||
|
connectTimeout: timeoutMs,
|
||||||
|
bodyTimeout: timeoutMs,
|
||||||
|
headersTimeout: timeoutMs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
throw fetchError;
|
throw fetchError;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
const endTime = performance.now();
|
|
||||||
|
|
||||||
// Logger la requête côté serveur
|
|
||||||
await DebugService.logRequest({
|
|
||||||
url: url,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
fromCache: false, // Côté serveur, on ne peut pas détecter le cache navigateur
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new AppError(ERROR_CODES.KOMGA.HTTP_ERROR, {
|
throw new AppError(ERROR_CODES.KOMGA.HTTP_ERROR, {
|
||||||
@@ -172,16 +177,6 @@ export abstract class BaseApiService {
|
|||||||
|
|
||||||
return options.isImage ? (response as T) : response.json();
|
return options.isImage ? (response as T) : response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const endTime = performance.now();
|
|
||||||
|
|
||||||
// Logger l'erreur côté serveur
|
|
||||||
await DebugService.logRequest({
|
|
||||||
url: url,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
fromCache: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export class BookService extends BaseApiService {
|
|||||||
const arrayBuffer = response.buffer.buffer.slice(
|
const arrayBuffer = response.buffer.buffer.slice(
|
||||||
response.buffer.byteOffset,
|
response.buffer.byteOffset,
|
||||||
response.buffer.byteOffset + response.buffer.byteLength
|
response.buffer.byteOffset + response.buffer.byteLength
|
||||||
);
|
) as ArrayBuffer;
|
||||||
|
|
||||||
return new Response(arrayBuffer, {
|
return new Response(arrayBuffer, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { DebugService } from "./debug.service";
|
|
||||||
import { getCurrentUser } from "../auth-utils";
|
import { getCurrentUser } from "../auth-utils";
|
||||||
import { ERROR_CODES } from "../../constants/errorCodes";
|
import { ERROR_CODES } from "../../constants/errorCodes";
|
||||||
import { AppError } from "../../utils/errors";
|
import { AppError } from "../../utils/errors";
|
||||||
@@ -50,12 +49,10 @@ export class ConfigDBService {
|
|||||||
try {
|
try {
|
||||||
const user: User | null = await this.getCurrentUser();
|
const user: User | null = await this.getCurrentUser();
|
||||||
|
|
||||||
return DebugService.measureMongoOperation("getConfig", async () => {
|
|
||||||
const config = await prisma.komgaConfig.findUnique({
|
const config = await prisma.komgaConfig.findUnique({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
});
|
});
|
||||||
return config as KomgaConfig | null;
|
return config as KomgaConfig | null;
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -68,12 +65,10 @@ export class ConfigDBService {
|
|||||||
try {
|
try {
|
||||||
const user: User | null = await this.getCurrentUser();
|
const user: User | null = await this.getCurrentUser();
|
||||||
|
|
||||||
return DebugService.measureMongoOperation("getTTLConfig", async () => {
|
|
||||||
const config = await prisma.tTLConfig.findUnique({
|
const config = await prisma.tTLConfig.findUnique({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
});
|
});
|
||||||
return config as TTLConfig | null;
|
return config as TTLConfig | null;
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -86,7 +81,6 @@ export class ConfigDBService {
|
|||||||
try {
|
try {
|
||||||
const user: User | null = await this.getCurrentUser();
|
const user: User | null = await this.getCurrentUser();
|
||||||
|
|
||||||
return DebugService.measureMongoOperation("saveTTLConfig", async () => {
|
|
||||||
const config = await prisma.tTLConfig.upsert({
|
const config = await prisma.tTLConfig.upsert({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
update: {
|
update: {
|
||||||
@@ -109,7 +103,6 @@ export class ConfigDBService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return config as TTLConfig;
|
return config as TTLConfig;
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -1,326 +0,0 @@
|
|||||||
import fs from "fs/promises";
|
|
||||||
import path from "path";
|
|
||||||
import type { RequestTiming } from "@/types/debug";
|
|
||||||
import { PreferencesService } from "./preferences.service";
|
|
||||||
import { getCurrentUser } from "../auth-utils";
|
|
||||||
import { ERROR_CODES } from "../../constants/errorCodes";
|
|
||||||
import { AppError } from "../../utils/errors";
|
|
||||||
|
|
||||||
export type { RequestTiming };
|
|
||||||
|
|
||||||
export class DebugService {
|
|
||||||
private static writeQueues = new Map<string, Promise<void>>();
|
|
||||||
|
|
||||||
private static async getCurrentUserId(): Promise<string> {
|
|
||||||
const user = await getCurrentUser();
|
|
||||||
if (!user) {
|
|
||||||
throw new AppError(ERROR_CODES.AUTH.UNAUTHENTICATED);
|
|
||||||
}
|
|
||||||
return user.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static getLogFilePath(userId: string): string {
|
|
||||||
return path.join(process.cwd(), "debug-logs", `${userId}.json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async ensureDebugDir(): Promise<void> {
|
|
||||||
const debugDir = path.join(process.cwd(), "debug-logs");
|
|
||||||
try {
|
|
||||||
await fs.access(debugDir);
|
|
||||||
} catch {
|
|
||||||
await fs.mkdir(debugDir, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async isDebugEnabled(): Promise<boolean> {
|
|
||||||
const user = await getCurrentUser();
|
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const preferences = await PreferencesService.getPreferences();
|
|
||||||
return preferences.debug === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async readLogs(filePath: string): Promise<RequestTiming[]> {
|
|
||||||
try {
|
|
||||||
const content = await fs.readFile(filePath, "utf-8");
|
|
||||||
return JSON.parse(content);
|
|
||||||
} catch {
|
|
||||||
// Essayer de lire un fichier de sauvegarde
|
|
||||||
try {
|
|
||||||
const backupPath = filePath + '.backup';
|
|
||||||
const backupContent = await fs.readFile(backupPath, "utf-8");
|
|
||||||
return JSON.parse(backupContent);
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async writeLogs(filePath: string, logs: RequestTiming[]): Promise<void> {
|
|
||||||
// Obtenir la queue existante ou créer une nouvelle
|
|
||||||
const existingQueue = this.writeQueues.get(filePath);
|
|
||||||
|
|
||||||
// Créer une nouvelle promesse qui attend la queue précédente
|
|
||||||
const newQueue = existingQueue
|
|
||||||
? existingQueue.then(() => this.performAppend(filePath, logs))
|
|
||||||
: this.performAppend(filePath, logs);
|
|
||||||
|
|
||||||
// Mettre à jour la queue
|
|
||||||
this.writeQueues.set(filePath, newQueue);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await newQueue;
|
|
||||||
} finally {
|
|
||||||
// Nettoyer la queue si c'est la dernière opération
|
|
||||||
if (this.writeQueues.get(filePath) === newQueue) {
|
|
||||||
this.writeQueues.delete(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async performAppend(filePath: string, logs: RequestTiming[]): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Lire le fichier existant
|
|
||||||
const existingLogs = await this.readLogs(filePath);
|
|
||||||
|
|
||||||
// Fusionner avec les nouveaux logs
|
|
||||||
const allLogs = [...existingLogs, ...logs];
|
|
||||||
|
|
||||||
// Garder seulement les 1000 derniers logs
|
|
||||||
const trimmedLogs = allLogs.slice(-1000);
|
|
||||||
|
|
||||||
// Créer une sauvegarde avant d'écrire
|
|
||||||
try {
|
|
||||||
await fs.copyFile(filePath, filePath + '.backup');
|
|
||||||
} catch {
|
|
||||||
// Ignorer si le fichier n'existe pas encore
|
|
||||||
}
|
|
||||||
|
|
||||||
// Écrire le fichier complet (c'est nécessaire pour maintenir l'ordre chronologique)
|
|
||||||
await fs.writeFile(filePath, JSON.stringify(trimmedLogs, null, 2), { flag: 'w' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Erreur lors de l'écriture des logs pour ${filePath}:`, error);
|
|
||||||
// Ne pas relancer l'erreur pour éviter de casser l'application
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async appendLog(filePath: string, log: RequestTiming): Promise<void> {
|
|
||||||
// Obtenir la queue existante ou créer une nouvelle
|
|
||||||
const existingQueue = this.writeQueues.get(filePath);
|
|
||||||
|
|
||||||
// Créer une nouvelle promesse qui attend la queue précédente
|
|
||||||
const newQueue = existingQueue
|
|
||||||
? existingQueue.then(() => this.performSingleAppend(filePath, log))
|
|
||||||
: this.performSingleAppend(filePath, log);
|
|
||||||
|
|
||||||
// Mettre à jour la queue
|
|
||||||
this.writeQueues.set(filePath, newQueue);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await newQueue;
|
|
||||||
} finally {
|
|
||||||
// Nettoyer la queue si c'est la dernière opération
|
|
||||||
if (this.writeQueues.get(filePath) === newQueue) {
|
|
||||||
this.writeQueues.delete(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async performSingleAppend(filePath: string, log: RequestTiming): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Lire le fichier existant
|
|
||||||
const existingLogs = await this.readLogs(filePath);
|
|
||||||
|
|
||||||
// Vérifier les doublons avec des tolérances différentes selon le type
|
|
||||||
const isPageRender = log.pageRender !== undefined;
|
|
||||||
const timeTolerance = isPageRender ? 500 : 50; // 500ms pour les rendus, 50ms pour les requêtes
|
|
||||||
|
|
||||||
const exists = existingLogs.some(existingLog =>
|
|
||||||
existingLog.url === log.url &&
|
|
||||||
Math.abs(existingLog.duration - log.duration) < 10 && // Durée similaire (10ms de tolérance)
|
|
||||||
Math.abs(new Date(existingLog.timestamp).getTime() - new Date(log.timestamp).getTime()) < timeTolerance
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
// Ajouter le nouveau log
|
|
||||||
const allLogs = [...existingLogs, log];
|
|
||||||
|
|
||||||
// Garder seulement les 1000 derniers logs
|
|
||||||
const trimmedLogs = allLogs.slice(-1000);
|
|
||||||
|
|
||||||
// Écrire le fichier complet avec gestion d'erreur
|
|
||||||
await fs.writeFile(filePath, JSON.stringify(trimmedLogs, null, 2), { flag: 'w' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Erreur lors de l'écriture du log pour ${filePath}:`, error);
|
|
||||||
// Ne pas relancer l'erreur pour éviter de casser l'application
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static createTiming(
|
|
||||||
url: string,
|
|
||||||
startTime: number,
|
|
||||||
endTime: number,
|
|
||||||
fromCache: boolean,
|
|
||||||
additionalData?: Partial<RequestTiming>
|
|
||||||
): RequestTiming {
|
|
||||||
return {
|
|
||||||
url,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
duration: endTime - startTime,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
fromCache,
|
|
||||||
...additionalData,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static async logRequest(timing: Omit<RequestTiming, "duration" | "timestamp">) {
|
|
||||||
try {
|
|
||||||
if (!(await this.isDebugEnabled())) return;
|
|
||||||
|
|
||||||
const userId = await this.getCurrentUserId();
|
|
||||||
await this.ensureDebugDir();
|
|
||||||
const filePath = this.getLogFilePath(userId);
|
|
||||||
|
|
||||||
const newTiming = this.createTiming(
|
|
||||||
timing.url,
|
|
||||||
timing.startTime,
|
|
||||||
timing.endTime,
|
|
||||||
timing.fromCache,
|
|
||||||
{
|
|
||||||
cacheType: timing.cacheType,
|
|
||||||
mongoAccess: timing.mongoAccess,
|
|
||||||
pageRender: timing.pageRender,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Utiliser un système d'append atomique
|
|
||||||
await this.appendLog(filePath, newTiming);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de l'enregistrement du log:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getRequestLogs(): Promise<RequestTiming[]> {
|
|
||||||
try {
|
|
||||||
const userId = await this.getCurrentUserId();
|
|
||||||
const filePath = this.getLogFilePath(userId);
|
|
||||||
return await this.readLogs(filePath);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof AppError) throw error;
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async clearLogs(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const userId = await this.getCurrentUserId();
|
|
||||||
const filePath = this.getLogFilePath(userId);
|
|
||||||
await this.clearFile(filePath);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof AppError) throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async clearFile(filePath: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Obtenir la queue existante ou créer une nouvelle
|
|
||||||
const existingQueue = this.writeQueues.get(filePath);
|
|
||||||
|
|
||||||
// Créer une nouvelle promesse qui attend la queue précédente
|
|
||||||
const newQueue = existingQueue
|
|
||||||
? existingQueue.then(() => this.performClear(filePath))
|
|
||||||
: this.performClear(filePath);
|
|
||||||
|
|
||||||
// Mettre à jour la queue
|
|
||||||
this.writeQueues.set(filePath, newQueue);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await newQueue;
|
|
||||||
} finally {
|
|
||||||
// Nettoyer la queue si c'est la dernière opération
|
|
||||||
if (this.writeQueues.get(filePath) === newQueue) {
|
|
||||||
this.writeQueues.delete(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Erreur lors du vidage du fichier ${filePath}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async performClear(filePath: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Créer une sauvegarde avant de vider
|
|
||||||
try {
|
|
||||||
await fs.copyFile(filePath, filePath + '.backup');
|
|
||||||
} catch {
|
|
||||||
// Ignorer si le fichier n'existe pas encore
|
|
||||||
}
|
|
||||||
|
|
||||||
// Écrire un tableau vide pour vider le fichier
|
|
||||||
await fs.writeFile(filePath, JSON.stringify([], null, 2), { flag: 'w' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Erreur lors du vidage du fichier ${filePath}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async logPageRender(page: string, duration: number) {
|
|
||||||
try {
|
|
||||||
if (!(await this.isDebugEnabled())) return;
|
|
||||||
|
|
||||||
const userId = await this.getCurrentUserId();
|
|
||||||
await this.ensureDebugDir();
|
|
||||||
const filePath = this.getLogFilePath(userId);
|
|
||||||
|
|
||||||
const now = performance.now();
|
|
||||||
const newTiming = this.createTiming(`Page Render: ${page}`, now - duration, now, false, {
|
|
||||||
pageRender: { page, duration },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Utiliser le même système d'append atomique
|
|
||||||
await this.appendLog(filePath, newTiming);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de l'enregistrement du log de rendu:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async measureMongoOperation<T>(operation: string, func: () => Promise<T>): Promise<T> {
|
|
||||||
const startTime = performance.now();
|
|
||||||
try {
|
|
||||||
if (!(await this.isDebugEnabled())) {
|
|
||||||
return func();
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await func();
|
|
||||||
const endTime = performance.now();
|
|
||||||
|
|
||||||
await this.logRequest({
|
|
||||||
url: `MongoDB: ${operation}`,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
fromCache: false,
|
|
||||||
mongoAccess: {
|
|
||||||
operation,
|
|
||||||
duration: endTime - startTime,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
const endTime = performance.now();
|
|
||||||
await this.logRequest({
|
|
||||||
url: `MongoDB Error: ${operation}`,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
fromCache: false,
|
|
||||||
mongoAccess: {
|
|
||||||
operation,
|
|
||||||
duration: endTime - startTime,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { DebugService } from "./debug.service";
|
|
||||||
import { getCurrentUser } from "../auth-utils";
|
import { getCurrentUser } from "../auth-utils";
|
||||||
import { ERROR_CODES } from "../../constants/errorCodes";
|
import { ERROR_CODES } from "../../constants/errorCodes";
|
||||||
import { AppError } from "../../utils/errors";
|
import { AppError } from "../../utils/errors";
|
||||||
@@ -30,7 +29,6 @@ export class FavoriteService {
|
|||||||
try {
|
try {
|
||||||
const user = await this.getCurrentUser();
|
const user = await this.getCurrentUser();
|
||||||
|
|
||||||
return DebugService.measureMongoOperation("isFavorite", async () => {
|
|
||||||
const favorite = await prisma.favorite.findFirst({
|
const favorite = await prisma.favorite.findFirst({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -38,7 +36,6 @@ export class FavoriteService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
return !!favorite;
|
return !!favorite;
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la vérification du favori:", error);
|
console.error("Erreur lors de la vérification du favori:", error);
|
||||||
return false;
|
return false;
|
||||||
@@ -52,7 +49,6 @@ export class FavoriteService {
|
|||||||
try {
|
try {
|
||||||
const user = await this.getCurrentUser();
|
const user = await this.getCurrentUser();
|
||||||
|
|
||||||
await DebugService.measureMongoOperation("addToFavorites", async () => {
|
|
||||||
await prisma.favorite.upsert({
|
await prisma.favorite.upsert({
|
||||||
where: {
|
where: {
|
||||||
userId_seriesId: {
|
userId_seriesId: {
|
||||||
@@ -66,7 +62,6 @@ export class FavoriteService {
|
|||||||
seriesId,
|
seriesId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
this.dispatchFavoritesChanged();
|
this.dispatchFavoritesChanged();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -81,14 +76,12 @@ export class FavoriteService {
|
|||||||
try {
|
try {
|
||||||
const user = await this.getCurrentUser();
|
const user = await this.getCurrentUser();
|
||||||
|
|
||||||
await DebugService.measureMongoOperation("removeFromFavorites", async () => {
|
|
||||||
await prisma.favorite.deleteMany({
|
await prisma.favorite.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
seriesId,
|
seriesId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
this.dispatchFavoritesChanged();
|
this.dispatchFavoritesChanged();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -102,19 +95,16 @@ export class FavoriteService {
|
|||||||
static async getAllFavoriteIds(): Promise<string[]> {
|
static async getAllFavoriteIds(): Promise<string[]> {
|
||||||
const user = await this.getCurrentUser();
|
const user = await this.getCurrentUser();
|
||||||
|
|
||||||
return DebugService.measureMongoOperation("getAllFavoriteIds", async () => {
|
|
||||||
const favorites = await prisma.favorite.findMany({
|
const favorites = await prisma.favorite.findMany({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
select: { seriesId: true },
|
select: { seriesId: true },
|
||||||
});
|
});
|
||||||
return favorites.map((favorite) => favorite.seriesId);
|
return favorites.map((favorite) => favorite.seriesId);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async addFavorite(seriesId: string) {
|
static async addFavorite(seriesId: string) {
|
||||||
const user = await this.getCurrentUser();
|
const user = await this.getCurrentUser();
|
||||||
|
|
||||||
return DebugService.measureMongoOperation("addFavorite", async () => {
|
|
||||||
const favorite = await prisma.favorite.upsert({
|
const favorite = await prisma.favorite.upsert({
|
||||||
where: {
|
where: {
|
||||||
userId_seriesId: {
|
userId_seriesId: {
|
||||||
@@ -129,13 +119,11 @@ export class FavoriteService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
return favorite;
|
return favorite;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async removeFavorite(seriesId: string): Promise<boolean> {
|
static async removeFavorite(seriesId: string): Promise<boolean> {
|
||||||
const user = await this.getCurrentUser();
|
const user = await this.getCurrentUser();
|
||||||
|
|
||||||
return DebugService.measureMongoOperation("removeFavorite", async () => {
|
|
||||||
const result = await prisma.favorite.deleteMany({
|
const result = await prisma.favorite.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -143,6 +131,5 @@ export class FavoriteService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
return result.count > 0;
|
return result.count > 0;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,9 +84,13 @@ export class LibraryService extends BaseApiService {
|
|||||||
try {
|
try {
|
||||||
// Récupérer toutes les séries depuis le cache
|
// Récupérer toutes les séries depuis le cache
|
||||||
const allSeries = await this.getAllLibrarySeries(libraryId);
|
const allSeries = await this.getAllLibrarySeries(libraryId);
|
||||||
|
|
||||||
// Filtrer les séries
|
// Filtrer les séries
|
||||||
let filteredSeries = allSeries;
|
let filteredSeries = allSeries;
|
||||||
|
|
||||||
|
// Filtrer les séries supprimées (fichiers manquants sur le filesystem)
|
||||||
|
filteredSeries = filteredSeries.filter((series) => !series.deleted);
|
||||||
|
|
||||||
if (unreadOnly) {
|
if (unreadOnly) {
|
||||||
filteredSeries = filteredSeries.filter(
|
filteredSeries = filteredSeries.filter(
|
||||||
(series) => series.booksReadCount < series.booksCount
|
(series) => series.booksReadCount < series.booksCount
|
||||||
@@ -96,7 +100,8 @@ export class LibraryService extends BaseApiService {
|
|||||||
if (search) {
|
if (search) {
|
||||||
const searchLower = search.toLowerCase();
|
const searchLower = search.toLowerCase();
|
||||||
filteredSeries = filteredSeries.filter((series) =>
|
filteredSeries = filteredSeries.filter((series) =>
|
||||||
series.metadata.title.toLowerCase().includes(searchLower)
|
series.metadata.title.toLowerCase().includes(searchLower) ||
|
||||||
|
series.id.toLowerCase().includes(searchLower)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +113,7 @@ export class LibraryService extends BaseApiService {
|
|||||||
const totalPages = Math.ceil(totalElements / size);
|
const totalPages = Math.ceil(totalElements / size);
|
||||||
const startIndex = page * size;
|
const startIndex = page * size;
|
||||||
const endIndex = Math.min(startIndex + size, totalElements);
|
const endIndex = Math.min(startIndex + size, totalElements);
|
||||||
|
|
||||||
const paginatedSeries = filteredSeries.slice(startIndex, endIndex);
|
const paginatedSeries = filteredSeries.slice(startIndex, endIndex);
|
||||||
|
|
||||||
// Construire la réponse
|
// Construire la réponse
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export class PreferencesService {
|
|||||||
showThumbnails: preferences.showThumbnails,
|
showThumbnails: preferences.showThumbnails,
|
||||||
cacheMode: preferences.cacheMode as "memory" | "file",
|
cacheMode: preferences.cacheMode as "memory" | "file",
|
||||||
showOnlyUnread: preferences.showOnlyUnread,
|
showOnlyUnread: preferences.showOnlyUnread,
|
||||||
debug: preferences.debug,
|
|
||||||
displayMode: preferences.displayMode as UserPreferences["displayMode"],
|
displayMode: preferences.displayMode as UserPreferences["displayMode"],
|
||||||
background: preferences.background as unknown as BackgroundPreferences,
|
background: preferences.background as unknown as BackgroundPreferences,
|
||||||
};
|
};
|
||||||
@@ -51,7 +50,6 @@ export class PreferencesService {
|
|||||||
if (preferences.showThumbnails !== undefined) updateData.showThumbnails = preferences.showThumbnails;
|
if (preferences.showThumbnails !== undefined) updateData.showThumbnails = preferences.showThumbnails;
|
||||||
if (preferences.cacheMode !== undefined) updateData.cacheMode = preferences.cacheMode;
|
if (preferences.cacheMode !== undefined) updateData.cacheMode = preferences.cacheMode;
|
||||||
if (preferences.showOnlyUnread !== undefined) updateData.showOnlyUnread = preferences.showOnlyUnread;
|
if (preferences.showOnlyUnread !== undefined) updateData.showOnlyUnread = preferences.showOnlyUnread;
|
||||||
if (preferences.debug !== undefined) updateData.debug = preferences.debug;
|
|
||||||
if (preferences.displayMode !== undefined) updateData.displayMode = preferences.displayMode;
|
if (preferences.displayMode !== undefined) updateData.displayMode = preferences.displayMode;
|
||||||
if (preferences.background !== undefined) updateData.background = preferences.background;
|
if (preferences.background !== undefined) updateData.background = preferences.background;
|
||||||
|
|
||||||
@@ -63,7 +61,6 @@ export class PreferencesService {
|
|||||||
showThumbnails: preferences.showThumbnails ?? defaultPreferences.showThumbnails,
|
showThumbnails: preferences.showThumbnails ?? defaultPreferences.showThumbnails,
|
||||||
cacheMode: preferences.cacheMode ?? defaultPreferences.cacheMode,
|
cacheMode: preferences.cacheMode ?? defaultPreferences.cacheMode,
|
||||||
showOnlyUnread: preferences.showOnlyUnread ?? defaultPreferences.showOnlyUnread,
|
showOnlyUnread: preferences.showOnlyUnread ?? defaultPreferences.showOnlyUnread,
|
||||||
debug: preferences.debug ?? defaultPreferences.debug,
|
|
||||||
displayMode: preferences.displayMode ?? defaultPreferences.displayMode,
|
displayMode: preferences.displayMode ?? defaultPreferences.displayMode,
|
||||||
background: (preferences.background ?? defaultPreferences.background) as unknown as Prisma.InputJsonValue,
|
background: (preferences.background ?? defaultPreferences.background) as unknown as Prisma.InputJsonValue,
|
||||||
},
|
},
|
||||||
@@ -73,7 +70,6 @@ export class PreferencesService {
|
|||||||
showThumbnails: updatedPreferences.showThumbnails,
|
showThumbnails: updatedPreferences.showThumbnails,
|
||||||
cacheMode: updatedPreferences.cacheMode as "memory" | "file",
|
cacheMode: updatedPreferences.cacheMode as "memory" | "file",
|
||||||
showOnlyUnread: updatedPreferences.showOnlyUnread,
|
showOnlyUnread: updatedPreferences.showOnlyUnread,
|
||||||
debug: updatedPreferences.debug,
|
|
||||||
displayMode: updatedPreferences.displayMode as UserPreferences["displayMode"],
|
displayMode: updatedPreferences.displayMode as UserPreferences["displayMode"],
|
||||||
background: updatedPreferences.background as unknown as BackgroundPreferences,
|
background: updatedPreferences.background as unknown as BackgroundPreferences,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ class RequestQueue {
|
|||||||
private activeCount = 0;
|
private activeCount = 0;
|
||||||
private maxConcurrent: number;
|
private maxConcurrent: number;
|
||||||
|
|
||||||
constructor(maxConcurrent: number = 5) {
|
constructor(maxConcurrent?: number) {
|
||||||
this.maxConcurrent = maxConcurrent;
|
// Lire depuis env ou utiliser la valeur par défaut
|
||||||
|
const envValue = process.env.KOMGA_MAX_CONCURRENT_REQUESTS;
|
||||||
|
this.maxConcurrent = maxConcurrent ?? (envValue ? parseInt(envValue, 10) : 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
async enqueue<T>(execute: () => Promise<T>): Promise<T> {
|
async enqueue<T>(execute: () => Promise<T>): Promise<T> {
|
||||||
@@ -68,6 +70,10 @@ class RequestQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton instance - Limite à 2 requêtes simultanées vers Komga (réduit pour CPU)
|
// Singleton instance - Par défaut limite à 2 requêtes simultanées (configurable via KOMGA_MAX_CONCURRENT_REQUESTS)
|
||||||
export const RequestQueueService = new RequestQueue(2);
|
export const RequestQueueService = new RequestQueue(
|
||||||
|
process.env.KOMGA_MAX_CONCURRENT_REQUESTS
|
||||||
|
? parseInt(process.env.KOMGA_MAX_CONCURRENT_REQUESTS, 10)
|
||||||
|
: 2
|
||||||
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ export class SeriesService extends BaseApiService {
|
|||||||
// Filtrer les livres
|
// Filtrer les livres
|
||||||
let filteredBooks = allBooks;
|
let filteredBooks = allBooks;
|
||||||
|
|
||||||
|
// Filtrer les livres supprimés (fichiers manquants sur le filesystem)
|
||||||
|
filteredBooks = filteredBooks.filter((book: KomgaBook) => !book.deleted);
|
||||||
|
|
||||||
if (unreadOnly) {
|
if (unreadOnly) {
|
||||||
filteredBooks = filteredBooks.filter(
|
filteredBooks = filteredBooks.filter(
|
||||||
(book: KomgaBook) => !book.readProgress || !book.readProgress.completed
|
(book: KomgaBook) => !book.readProgress || !book.readProgress.completed
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { PreferencesService } from "./preferences.service";
|
import { PreferencesService } from "./preferences.service";
|
||||||
import { DebugService } from "./debug.service";
|
|
||||||
import { getCurrentUser } from "../auth-utils";
|
import { getCurrentUser } from "../auth-utils";
|
||||||
|
|
||||||
export type CacheMode = "file" | "memory";
|
export type CacheMode = "file" | "memory";
|
||||||
@@ -440,14 +439,13 @@ class ServerCacheService {
|
|||||||
const { data, isStale } = cachedResult;
|
const { data, isStale } = cachedResult;
|
||||||
const endTime = performance.now();
|
const endTime = performance.now();
|
||||||
|
|
||||||
// Log la requête avec l'indication du cache
|
// Debug logging
|
||||||
await DebugService.logRequest({
|
if (process.env.CACHE_DEBUG === 'true') {
|
||||||
url: `[CACHE${isStale ? '-STALE' : ''}] ${key}`,
|
const icon = isStale ? '⚠️' : '✅';
|
||||||
startTime,
|
const status = isStale ? 'STALE' : 'HIT';
|
||||||
endTime,
|
// eslint-disable-next-line no-console
|
||||||
fromCache: true,
|
console.log(`${icon} [CACHE ${status}] ${key} | ${type} | ${(endTime - startTime).toFixed(2)}ms`);
|
||||||
cacheType: type,
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Si le cache est expiré, revalider en background sans bloquer la réponse
|
// Si le cache est expiré, revalider en background sans bloquer la réponse
|
||||||
if (isStale) {
|
if (isStale) {
|
||||||
@@ -459,9 +457,21 @@ class ServerCacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pas de cache du tout, fetch normalement
|
// Pas de cache du tout, fetch normalement
|
||||||
|
if (process.env.CACHE_DEBUG === 'true') {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`❌ [CACHE MISS] ${key} | ${type}`);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetcher();
|
const data = await fetcher();
|
||||||
this.set(cacheKey, data, type);
|
this.set(cacheKey, data, type);
|
||||||
|
|
||||||
|
const endTime = performance.now();
|
||||||
|
if (process.env.CACHE_DEBUG === 'true') {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`💾 [CACHE SET] ${key} | ${type} | ${(endTime - startTime).toFixed(2)}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -482,16 +492,13 @@ class ServerCacheService {
|
|||||||
const data = await fetcher();
|
const data = await fetcher();
|
||||||
this.set(cacheKey, data, type);
|
this.set(cacheKey, data, type);
|
||||||
|
|
||||||
|
if (process.env.CACHE_DEBUG === 'true') {
|
||||||
const endTime = performance.now();
|
const endTime = performance.now();
|
||||||
await DebugService.logRequest({
|
// eslint-disable-next-line no-console
|
||||||
url: `[REVALIDATE] ${debugKey}`,
|
console.log(`🔄 [CACHE REVALIDATE] ${debugKey} | ${type} | ${(endTime - startTime).toFixed(2)}ms`);
|
||||||
startTime,
|
}
|
||||||
endTime,
|
|
||||||
fromCache: false,
|
|
||||||
cacheType: type,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Background revalidation failed for ${debugKey}:`, error);
|
console.error(`🔴 [CACHE REVALIDATE ERROR] ${debugKey}:`, error);
|
||||||
// Ne pas relancer l'erreur car c'est en background
|
// Ne pas relancer l'erreur car c'est en background
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
// Wrapper pour détecter le cache du navigateur
|
|
||||||
export async function fetchWithCacheDetection(url: string, options: RequestInit = {}) {
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, options);
|
|
||||||
const endTime = performance.now();
|
|
||||||
|
|
||||||
// Détecter si la réponse vient du cache du navigateur
|
|
||||||
const fromBrowserCache = response.headers.get('x-cache') === 'HIT' ||
|
|
||||||
response.headers.get('cf-cache-status') === 'HIT' ||
|
|
||||||
(endTime - startTime) < 5; // Si très rapide, probablement du cache
|
|
||||||
|
|
||||||
// Logger la requête seulement si ce n'est pas une requête de debug
|
|
||||||
// Note: La vérification du mode debug se fait côté serveur dans DebugService
|
|
||||||
if (!url.includes('/api/debug')) {
|
|
||||||
try {
|
|
||||||
await fetch("/api/debug", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
url: url,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
fromCache: fromBrowserCache,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// Ignorer les erreurs de logging
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
const endTime = performance.now();
|
|
||||||
|
|
||||||
// Logger aussi les erreurs seulement si ce n'est pas une requête de debug
|
|
||||||
if (!url.includes('/api/debug')) {
|
|
||||||
try {
|
|
||||||
await fetch("/api/debug", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
url: url,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
fromCache: false,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// Ignorer les erreurs de logging
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import type { CacheType } from "./cache";
|
|
||||||
|
|
||||||
export interface RequestTiming {
|
|
||||||
url: string;
|
|
||||||
startTime: number;
|
|
||||||
endTime: number;
|
|
||||||
duration: number;
|
|
||||||
timestamp: string;
|
|
||||||
fromCache: boolean;
|
|
||||||
cacheType?: CacheType;
|
|
||||||
mongoAccess?: {
|
|
||||||
operation: string;
|
|
||||||
duration: number;
|
|
||||||
};
|
|
||||||
pageRender?: {
|
|
||||||
page: string;
|
|
||||||
duration: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -121,6 +121,7 @@ export interface KomgaBook {
|
|||||||
media: BookMedia;
|
media: BookMedia;
|
||||||
metadata: BookMetadata;
|
metadata: BookMetadata;
|
||||||
readProgress: ReadProgress | null;
|
readProgress: ReadProgress | null;
|
||||||
|
deleted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BookMedia {
|
export interface BookMedia {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export interface UserPreferences {
|
|||||||
showThumbnails: boolean;
|
showThumbnails: boolean;
|
||||||
cacheMode: "memory" | "file";
|
cacheMode: "memory" | "file";
|
||||||
showOnlyUnread: boolean;
|
showOnlyUnread: boolean;
|
||||||
debug: boolean;
|
|
||||||
displayMode: {
|
displayMode: {
|
||||||
compact: boolean;
|
compact: boolean;
|
||||||
itemsPerPage: number;
|
itemsPerPage: number;
|
||||||
@@ -22,7 +21,6 @@ export const defaultPreferences: UserPreferences = {
|
|||||||
showThumbnails: true,
|
showThumbnails: true,
|
||||||
cacheMode: "memory",
|
cacheMode: "memory",
|
||||||
showOnlyUnread: false,
|
showOnlyUnread: false,
|
||||||
debug: false,
|
|
||||||
displayMode: {
|
displayMode: {
|
||||||
compact: false,
|
compact: false,
|
||||||
itemsPerPage: 20,
|
itemsPerPage: 20,
|
||||||
|
|||||||
23
src/utils/image-errors.ts
Normal file
23
src/utils/image-errors.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { AppError } from "./errors";
|
||||||
|
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper pour trouver le status HTTP dans la chaîne d'erreurs imbriquées
|
||||||
|
* Parcourt récursivement les originalError pour trouver une erreur KOMGA.HTTP_ERROR
|
||||||
|
*/
|
||||||
|
export function findHttpStatus(error: unknown): number | null {
|
||||||
|
if (!(error instanceof AppError)) return null;
|
||||||
|
|
||||||
|
// Si c'est une erreur HTTP, récupérer le status
|
||||||
|
if (error.code === ERROR_CODES.KOMGA.HTTP_ERROR) {
|
||||||
|
return (error.params as any)?.status || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sinon, chercher récursivement dans originalError
|
||||||
|
if (error.originalError) {
|
||||||
|
return findHttpStatus(error.originalError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user