# 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({ level: "all", search: "", page: 1 }); const { data, isLoading, error, refetch, isMockData } = useAdminLogs(filters); return (
{/* Header avec FileText icon */} {/* Error banner si error */} {/* LogsFilters */} {/* LogsTable */} {/* Pagination */} {/* Info footer avec Mode Démo si isMockData */}
); } ``` ### 🔄 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)