# Story 5.7: Admin - Error Logs Viewer

Status: done

## Story

En tant qu'**Admin**,
Je veux **visualiser les logs d'erreurs structurés**,
Afin de **déboguer les problèmes système et les échecs de traduction**.

## Acceptance Criteria

1. **Page dédiée**: La page de logs est accessible sur `/admin/logs`
2. **Affichage structuré**: Les logs sont affichés sous forme de tableau avec colonnes: `timestamp`, `level`, `message`, `user_id`, `error_code`
3. **Filtre par niveau**: Un filtre permet de sélectionner: `error`, `warning`, `info` (ou `tous`)
4. **Recherche**: Un champ de recherche permet de filtrer par `error_code` ou `user_id`
5. **Source de données**: Les logs proviennent de `GET /api/v1/admin/logs` (à créer) qui retourne les enregistrements de traductions échouées + logs système
6. **Aucun contenu document**: Les logs n'affichent JAMAIS le contenu des fichiers traduits (NFR11, NFR16)
7. **Pagination**: Les logs sont paginés (50 entrées par page)
8. **Mode démo**: Si le backend ne retourne pas le bon format, afficher des données mock avec indicateur clair "Mode Démo"
9. **Actualisation manuelle**: Bouton "Actualiser" pour recharger les logs
10. **Gestion d'erreurs**: En cas d'erreur API, afficher un message en français clair

## Tasks / Subtasks

- [x] **Task 1: Créer le backend endpoint GET /api/v1/admin/logs** (AC: #5, #6, #7)
  - [x] 1.1 Ajouter dans `routes/admin_routes.py` la route `GET /api/v1/admin/logs`
  - [x] 1.2 La route requiert l'authentification admin (`Depends(require_admin)`)
  - [x] 1.3 Query params: `level` (all/error/warning/info), `search` (user_id ou error_code), `page` (défaut 1), `per_page` (défaut 50)
  - [x] 1.4 Retourner les traductions avec `status = "failed"` depuis la table `translations`
  - [x] 1.5 Mapper chaque entrée vers le format log: `{timestamp, level, message, user_id, error_code}`
  - [x] 1.6 S'assurer qu'aucun `original_filename` ni contenu de fichier n'est exposé
  - [x] 1.7 Réponse format: `{data: {logs: [...], total: N, page: N, per_page: N}, meta: {generated_at: "..."}}`

- [x] **Task 2: Créer les types TypeScript** (AC: #2, #5)
  - [x] 2.1 Créer `frontend/src/app/admin/logs/types.ts`
  - [x] 2.2 Définir `LogEntry`, `LogLevel`, `LogsResponse`, `LogsFilters` interfaces

- [x] **Task 3: Créer le hook useAdminLogs** (AC: #3, #4, #5, #8, #10)
  - [x] 3.1 Créer `frontend/src/app/admin/logs/useAdminLogs.ts`
  - [x] 3.2 Utiliser `useQuery` TanStack Query v5 avec key `["admin", "logs", filters]`
  - [x] 3.3 Accepter les params: `level`, `search`, `page`
  - [x] 3.4 Fallback vers données mock si endpoint retourne 404 (pattern existant)
  - [x] 3.5 Mapper les erreurs API vers messages français

- [x] **Task 4: Créer le composant LogsTable** (AC: #2, #6)
  - [x] 4.1 Créer `frontend/src/app/admin/logs/LogsTable.tsx`
  - [x] 4.2 Tableau avec colonnes: Niveau (badge coloré), Timestamp, Message, User ID, Error Code
  - [x] 4.3 Badge coloré selon niveau: rouge=error, orange=warning, bleu=info
  - [x] 4.4 Tronquer le message à 100 chars avec tooltip pour voir tout
  - [x] 4.5 `user_id` affiché tronqué (8 premiers chars) ou "Système" si null

- [x] **Task 5: Créer le composant LogsFilters** (AC: #3, #4)
  - [x] 5.1 Créer `frontend/src/app/admin/logs/LogsFilters.tsx`
  - [x] 5.2 Sélecteur de niveau (Tous / Erreur / Avertissement / Info)
  - [x] 5.3 Champ de recherche avec debounce 300ms
  - [x] 5.4 Afficher le count de résultats

- [x] **Task 6: Créer la page `/admin/logs`** (AC: #1, #7, #9, #10)
  - [x] 6.1 Créer `frontend/src/app/admin/logs/page.tsx` (⚠️ minuscule obligatoire)
  - [x] 6.2 Header avec icône FileText, titre "Logs d'Erreurs", description
  - [x] 6.3 Bouton "Actualiser" avec état loading
  - [x] 6.4 Intégrer LogsFilters + LogsTable
  - [x] 6.5 Pagination simple (Précédent / Page X/Y / Suivant)
  - [x] 6.6 Gestion erreur globale + indicateur Mode Démo si données mock
  - [x] 6.7 Message vide si aucun log correspondant aux filtres

- [x] **Task 7: Tests et validation** (AC: Tous)
  - [x] 7.1 `npm run build` → 0 erreurs TypeScript
  - [x] 7.2 Tester le filtre par niveau
  - [x] 7.3 Tester la recherche par user_id
  - [x] 7.4 Vérifier aucun contenu document dans les logs affichés
  - [x] 7.5 Vérifier que le lien "Logs" dans la sidebar est actif sur `/admin/logs`

## Dev Notes

### 🏗️ Stack Technique

| Technologie | Version |
|-------------|---------|
| Next.js | 16.0.6 (App Router) |
| React | 19.2.0 |
| TanStack Query | v5 |
| Tailwind CSS | configuré |
| shadcn/ui | Card, Button, Badge, Input, Select, Table, Tooltip |
| Lucide React | FileText, Search, RefreshCw, Loader2, AlertCircle, Info |

### 📁 Structure Cible (Colocation Pattern)

```
frontend/src/app/admin/logs/
├── page.tsx          # ⭐ Page principale (⚠️ TOUJOURS minuscule)
├── useAdminLogs.ts   # ⭐ Hook TanStack Query
├── LogsTable.tsx     # ⭐ Composant tableau des logs
├── LogsFilters.tsx   # ⭐ Composant filtres + recherche
└── types.ts          # ⭐ Interfaces TypeScript
```

**⚠️ Règle absolue (architecture.md):**
```
🚨 FICHIERS SPÉCIAUX: page.tsx, layout.tsx → TOUJOURS minuscules
🚨 COLOCATION: Components/hooks/types dans le dossier de leur page
🚨 COMPOSANTS: PascalCase (LogsTable.tsx, LogsFilters.tsx)
🚨 HOOKS: useCamelCase (useAdminLogs.ts)
```

### 🔗 API Endpoint (à créer côté backend)

| Endpoint | Méthode | Auth | Description |
|----------|---------|------|-------------|
| `/api/v1/admin/logs` | GET | Bearer token admin | Récupérer les logs paginés |

**Query parameters:**
| Param | Type | Défaut | Description |
|-------|------|--------|-------------|
| `level` | `all\|error\|warning\|info` | `all` | Filtre par niveau |
| `search` | `string` | `""` | Recherche par user_id ou error_code |
| `page` | `number` | `1` | Page courante |
| `per_page` | `number` | `50` | Entrées par page |

### 📊 Format de Réponse Backend

```json
{
  "data": {
    "logs": [
      {
        "timestamp": "2026-02-28T10:30:00Z",
        "level": "error",
        "message": "Translation failed: Provider unavailable",
        "user_id": "usr_abc123",
        "error_code": "PROVIDER_UNAVAILABLE",
        "provider": "google",
        "file_type": "xlsx"
      }
    ],
    "total": 142,
    "page": 1,
    "per_page": 50
  },
  "meta": {
    "generated_at": "2026-02-28T10:35:00Z"
  }
}
```

**⚠️ Champs INTERDITS dans les logs (NFR11, NFR16):**
- `original_filename` → JAMAIS exposé
- Contenu du document traduit → JAMAIS exposé
- Données personnelles sensibles → JAMAIS exposées

### 🔧 Implémentation Backend

Ajouter dans `routes/admin_routes.py`:

```python
@router.get("/logs")
async def get_admin_logs(
    is_admin: bool = Depends(require_admin),
    level: str = Query(default="all", pattern="^(all|error|warning|info)$"),
    search: str = Query(default=""),
    page: int = Query(default=1, ge=1),
    per_page: int = Query(default=50, ge=1, le=200),
    db: AsyncSession = Depends(get_db)
):
    """Get admin error logs from failed translations"""
    from database.models import Translation
    from sqlalchemy import select, func, desc
    
    # Build query - only failed translations for "error" logs
    query = select(Translation).where(Translation.status == "failed")
    
    # Level filter (all failed = error level)
    if level == "warning":
        # Could also include partial failures in future
        return {"data": {"logs": [], "total": 0, "page": page, "per_page": per_page}, "meta": {...}}
    elif level == "info":
        return {"data": {"logs": [], "total": 0, "page": page, "per_page": per_page}, "meta": {...}}
    
    # Search filter
    if search:
        query = query.where(
            or_(Translation.user_id.ilike(f"%{search}%"),
                Translation.error_message.ilike(f"%{search}%"))
        )
    
    # Pagination
    total = await db.scalar(select(func.count()).select_from(query.subquery()))
    query = query.order_by(desc(Translation.created_at)).offset((page - 1) * per_page).limit(per_page)
    result = await db.execute(query)
    translations = result.scalars().all()
    
    logs = [
        {
            "timestamp": t.created_at.isoformat() + "Z",
            "level": "error",
            "message": t.error_message or "Translation failed",
            "user_id": t.user_id,
            "error_code": _extract_error_code(t.error_message),
            "provider": t.provider,
            "file_type": t.file_type,
            # ⚠️ NEVER include original_filename
        }
        for t in translations
    ]
    
    return {
        "data": {
            "logs": logs,
            "total": total,
            "page": page,
            "per_page": per_page
        },
        "meta": {"generated_at": datetime.utcnow().isoformat() + "Z"}
    }
```

### 📊 Types TypeScript Cibles

```typescript
// frontend/src/app/admin/logs/types.ts

export type LogLevel = "all" | "error" | "warning" | "info";

export interface LogEntry {
  timestamp: string;
  level: "error" | "warning" | "info";
  message: string;
  user_id: string | null;
  error_code: string | null;
  provider?: string;
  file_type?: string;
}

export interface LogsData {
  logs: LogEntry[];
  total: number;
  page: number;
  per_page: number;
}

export interface LogsResponse {
  data: LogsData;
  meta: {
    generated_at: string;
  };
}

export interface LogsFilters {
  level: LogLevel;
  search: string;
  page: number;
}
```

### 🎨 Patterns UI à Réutiliser

**Badge coloré par niveau:**
```tsx
const levelConfig = {
  error: { label: "Erreur", className: "bg-red-500/10 text-red-500 border-red-200/30" },
  warning: { label: "Avertissement", className: "bg-orange-500/10 text-orange-500 border-orange-200/30" },
  info: { label: "Info", className: "bg-blue-500/10 text-blue-500 border-blue-200/30" },
};
```

**Pattern hook (reproduire `useTranslationStats.ts`):**
```typescript
// useAdminLogs.ts
"use client";
import { useQuery } from "@tanstack/react-query";
import { useTranslationStore } from "@/lib/store";
import { API_BASE } from "@/lib/config";
import type { LogsFilters, LogsResponse } from "./types";

export const QUERY_KEY = (filters: LogsFilters) => 
  ["admin", "logs", filters.level, filters.search, filters.page];

export function useAdminLogs(filters: LogsFilters) {
  const { settings } = useTranslationStore();
  
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: QUERY_KEY(filters),
    queryFn: async () => {
      try {
        const result = await fetchLogs(settings.adminToken, filters);
        return { ...result, isMock: false };
      } catch (err) {
        if ((err as Error).message === "ENDPOINT_NOT_FOUND") {
          return { data: getMockData(), isMock: true };
        }
        throw err;
      }
    },
    enabled: !!settings.adminToken,
    staleTime: 15000,
    retry: 1,
  });
  // ... error mapping vers messages français
}
```

**Page layout pattern (reproduire stats/page.tsx):**
```tsx
// page.tsx
"use client";
export default function LogsPage() {
  const [filters, setFilters] = useState<LogsFilters>({ level: "all", search: "", page: 1 });
  const { data, isLoading, error, refetch, isMockData } = useAdminLogs(filters);
  
  return (
    <div className="space-y-6">
      {/* Header avec FileText icon */}
      {/* Error banner si error */}
      {/* LogsFilters */}
      {/* LogsTable */}
      {/* Pagination */}
      {/* Info footer avec Mode Démo si isMockData */}
    </div>
  );
}
```

### 🔄 Flux de données

```
Admin ouvre /admin/logs
    ↓
useAdminLogs() → GET /api/v1/admin/logs?level=all&page=1
    ↓ (isLoading = true)
Backend: Query translations WHERE status = "failed"
    ↓
Response: { data: { logs: [...], total: 142 } }
    ↓
LogsTable affiche 50 entrées
    ↓
Admin filtre par "error" → refetch avec level=error
Admin cherche "user_123" → debounce 300ms → refetch avec search=user_123
Admin clique "Suivant" → refetch avec page=2
```

### ⚠️ Points d'Attention Critiques

1. **Sécurité des données (CRITIQUE)**: Ne JAMAIS exposer `original_filename` ou le contenu des documents. Les logs ne doivent contenir que des métadonnées techniques.

2. **Authentification**: Tous les appels API doivent inclure `Authorization: Bearer {adminToken}`. Le token est dans `useTranslationStore().settings.adminToken`.

3. **Sidebar déjà configurée**: Le lien "Logs" (`/admin/logs`) est déjà dans `constants.ts` avec l'icône `FileText`. La sidebar l'affiche automatiquement.

4. **Pattern mock data**: Si le backend retourne 404 sur `/api/v1/admin/logs`, fallback vers données mock avec indicateur "Mode Démo" (même pattern que `useTranslationStats.ts`).

5. **Debounce recherche**: Utiliser `useState` + `useEffect` avec `setTimeout(300ms)` pour éviter les requêtes excessives lors de la saisie.

6. **Reset pagination**: Quand les filtres changent (level ou search), reset `page` à `1` automatiquement.

7. **`"use client"` obligatoire**: La page utilise `useState` pour les filtres → doit être Client Component.

8. **DB Session async**: La route backend doit utiliser `AsyncSession` comme les autres routes admin (voir `admin_routes.py`). Si la DB est synchrone, adapter en conséquence.

9. **Pas de SQLAlchemy async si pas disponible**: Vérifier si `get_db` retourne sync ou async session en regardant `routes/deps.py`. Adapter l'implémentation backend.

### 📋 Checklist de Validation Avant Dev

- [ ] Vérifier si `get_db` (dans `routes/deps.py`) est synchrone ou asynchrone
- [ ] Vérifier si `Translation` model est importable depuis `database.models`
- [ ] Confirmer que `adminNavItems` dans `constants.ts` inclut bien `/admin/logs`
- [ ] Confirmer que les composants shadcn/ui `Table`, `Select`, `Input` sont disponibles

### 🔍 Vérification de la Sidebar

Le lien "Logs" est **déjà présent** dans `constants.ts`:
```typescript
{ label: 'Logs', href: '/admin/logs', icon: FileText }
```
La sidebar (`AdminSidebar.tsx`) l'affiche automatiquement via `adminNavItems.map(...)`. Aucune modification de la sidebar n'est nécessaire.

### 📚 Previous Story Intelligence (Story 5.6 - Admin Manual File Cleanup)

**Learnings from story 5.6:**
- Utiliser TanStack Query `useQuery` pour le data fetching, `useMutation` pour les actions
- Le token admin est dans `useTranslationStore().settings.adminToken`
- Pattern de colocation strict: composants/hooks/types dans `admin/logs/`
- Les composants shadcn/ui disponibles: Card, Button, Badge, Progress, Tooltip, Input
- Mapper systématiquement les erreurs API vers messages utilisateur en français
- `page.tsx` DOIT être en minuscules (sinon 404 Next.js)
- Utiliser `API_BASE` depuis `@/lib/config` pour l'URL de base
- Pattern double-fetch évité (voir fix de 5.6: ne pas appeler `refetch()` après mutation qui invalide déjà le cache)

**Fichiers de référence dans Story 5.6:**
- `frontend/src/app/admin/system/useSystemPage.ts` — pattern hook combiné
- `frontend/src/app/admin/system/CleanupSection.tsx` — pattern card avec action
- `frontend/src/app/admin/system/DiskSpaceCard.tsx` — pattern card metric

**Fichiers existants à réutiliser/importer:**
- `frontend/src/app/admin/useAdminDashboard.ts` — pattern hook useQuery à suivre
- `frontend/src/app/admin/useTranslationStats.ts` — **patron exact** pour mock data fallback
- `frontend/src/app/admin/types.ts` — types admin existants (ne pas redéfinir)
- `frontend/src/app/admin/DateRangeFilter.tsx` — exemple de composant filtre
- `frontend/src/app/admin/stats/page.tsx` — **patron exact** de page admin avec filtres

### 🔍 Git Intelligence (Derniers commits)

```
3d37ce4 feat: Update Docker and Kubernetes for database infrastructure
550f351 feat: Add PostgreSQL database infrastructure
c4d6cae Production-ready improvements: security hardening, Redis sessions
721b18d Restore provider selection, model selection, and context/glossary
dfd45d9 Fix admin login endpoint to accept JSON instead of form data
```

**Insights:**
- L'infrastructure base de données vient d'être mise à jour (PostgreSQL)
- Redis configuré pour les sessions admin
- La session admin utilise Redis (voir `admin_routes.py` → `get_redis_client()`)
- Le fix du login admin (JSON vs form data) → pattern à respecter pour les nouvelles routes

### References

- [Source: _bmad-output/planning-artifacts/epics.md#Story-5.7] — Story requirements (FR45)
- [Source: _bmad-output/planning-artifacts/architecture.md#Frontend-Architecture] — Colocation pattern, Next.js App Router rules
- [Source: _bmad-output/planning-artifacts/architecture.md#Infrastructure-Deployment] — structlog (prévu Epic 6)
- [Source: _bmad-output/planning-artifacts/architecture.md#API-Response-Formats] — Format `{data, meta}`
- [Source: database/models.py#Translation] — Modèle Translation avec status, error_message, user_id
- [Source: routes/admin_routes.py#require_admin] — Pattern authentification admin
- [Source: frontend/src/app/admin/constants.ts] — Sidebar nav (Logs lien déjà présent)
- [Source: frontend/src/app/admin/useTranslationStats.ts] — Pattern mock data fallback
- [Source: frontend/src/app/admin/stats/page.tsx] — Pattern page admin avec filtres

## Change Log

- 2026-02-28: Implémentation complète story 5-7 — Backend GET /api/v1/admin/logs, frontend page /admin/logs (types, useAdminLogs, LogsTable, LogsFilters, page.tsx). Pagination 50/page, filtres niveau/recherche, Mode Démo si 404, NFR11/NFR16 respectés.
- 2026-02-28: Code review (AI): correctifs appliqués (tests test_admin_logs.py, backend timestamp Z, search max_length 200, docstring, route sync). Recommandation: commiter les fichiers frontend admin/logs.

## Dev Agent Record

### Agent Model Used

À compléter par le dev agent

### Debug Log References

- Backend: route GET /api/v1/admin/logs dans routes/admin_routes.py avec get_sync_session, filtre status=failed, pagination, _extract_error_code pour error_code. Aucun original_filename ni contenu exposé.
- Frontend: types.ts, useAdminLogs (TanStack Query, mock fallback 404), LogsTable (badges niveau, tooltip message 100 chars, user_id tronqué/Système), LogsFilters (debounce 300ms), page.tsx (header FileText, Actualiser, pagination, Mode Démo).

### Completion Notes List

- Task 1: Endpoint GET /api/v1/admin/logs créé avec require_admin, query params level/search/page/per_page, réponse {data: {logs, total, page, per_page}, meta: {generated_at}}. NFR11/NFR16 respectés (pas d’original_filename ni contenu document).
- Tasks 2–6: Types, hook useAdminLogs, LogsTable, LogsFilters, page /admin/logs implémentés. Build npm run build OK. Lien Logs déjà dans constants.ts (sidebar).
- Code review 2026-02-28: Tests test_admin_logs.py ajoutés; backend corrigé (timestamp Z, search max_length 200, docstring, sync).

### File List

**Fichiers modifiés backend:**
- `routes/admin_routes.py` (route GET /api/v1/admin/logs + _extract_error_code)

**Nouveaux fichiers frontend:**
- `frontend/src/app/admin/logs/page.tsx`
- `frontend/src/app/admin/logs/useAdminLogs.ts`
- `frontend/src/app/admin/logs/LogsTable.tsx`
- `frontend/src/app/admin/logs/LogsFilters.tsx`
- `frontend/src/app/admin/logs/types.ts`

**Ajoutés après code review:**
- `tests/test_admin_logs.py` (tests automatisés: auth, shape, NFR11/NFR16, level, pagination)
