Files
office_translator/_bmad-output/implementation-artifacts/5-7-admin-error-logs-viewer.md
Sepehr Ramezani 26bd096a06 feat: production deployment - full update with providers, admin, glossaries, pricing, tests
Major changes across backend, frontend, infrastructure:
- Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud)
- Admin panel: user management, pricing, settings
- Glossary system with CSV import/export
- Subscription and tier quota management
- Security hardening (rate limiting, API key auth, path traversal fixes)
- Docker compose for dev, prod, and IONOS deployment
- Alembic migrations for new tables
- Frontend: dashboard, pricing page, landing page, i18n (en/fr)
- Test suite and verification scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-25 15:01:47 +02:00

19 KiB
Raw Blame History

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

  • Task 1: Créer le backend endpoint GET /api/v1/admin/logs (AC: #5, #6, #7)

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

    • 2.1 Créer frontend/src/app/admin/logs/types.ts
    • 2.2 Définir LogEntry, LogLevel, LogsResponse, LogsFilters interfaces
  • Task 3: Créer le hook useAdminLogs (AC: #3, #4, #5, #8, #10)

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

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

    • 5.1 Créer frontend/src/app/admin/logs/LogsFilters.tsx
    • 5.2 Sélecteur de niveau (Tous / Erreur / Avertissement / Info)
    • 5.3 Champ de recherche avec debounce 300ms
    • 5.4 Afficher le count de résultats
  • Task 6: Créer la page /admin/logs (AC: #1, #7, #9, #10)

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

    • 7.1 npm run build → 0 erreurs TypeScript
    • 7.2 Tester le filtre par niveau
    • 7.3 Tester la recherche par user_id
    • 7.4 Vérifier aucun contenu document dans les logs affichés
    • 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

{
  "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:

@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

// 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:

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):

// 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):

// 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:

{ 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.tspatron 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.tsxpatron 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.pyget_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 doriginal_filename ni contenu document).
  • Tasks 26: 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)