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>
19 KiB
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
- Page dédiée: La page de logs est accessible sur
/admin/logs - Affichage structuré: Les logs sont affichés sous forme de tableau avec colonnes:
timestamp,level,message,user_id,error_code - Filtre par niveau: Un filtre permet de sélectionner:
error,warning,info(outous) - Recherche: Un champ de recherche permet de filtrer par
error_codeouuser_id - 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 - Aucun contenu document: Les logs n'affichent JAMAIS le contenu des fichiers traduits (NFR11, NFR16)
- Pagination: Les logs sont paginés (50 entrées par page)
- Mode démo: Si le backend ne retourne pas le bon format, afficher des données mock avec indicateur clair "Mode Démo"
- Actualisation manuelle: Bouton "Actualiser" pour recharger les logs
- 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.pyla routeGET /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 tabletranslations - 1.5 Mapper chaque entrée vers le format log:
{timestamp, level, message, user_id, error_code} - 1.6 S'assurer qu'aucun
original_filenameni contenu de fichier n'est exposé - 1.7 Réponse format:
{data: {logs: [...], total: N, page: N, per_page: N}, meta: {generated_at: "..."}}
- 1.1 Ajouter dans
-
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,LogsFiltersinterfaces
- 2.1 Créer
-
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
useQueryTanStack 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
- 3.1 Créer
-
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_idaffiché tronqué (8 premiers chars) ou "Système" si null
- 4.1 Créer
-
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
- 5.1 Créer
-
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
- 6.1 Créer
-
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
- 7.1
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
-
Sécurité des données (CRITIQUE): Ne JAMAIS exposer
original_filenameou le contenu des documents. Les logs ne doivent contenir que des métadonnées techniques. -
Authentification: Tous les appels API doivent inclure
Authorization: Bearer {adminToken}. Le token est dansuseTranslationStore().settings.adminToken. -
Sidebar déjà configurée: Le lien "Logs" (
/admin/logs) est déjà dansconstants.tsavec l'icôneFileText. La sidebar l'affiche automatiquement. -
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 queuseTranslationStats.ts). -
Debounce recherche: Utiliser
useState+useEffectavecsetTimeout(300ms)pour éviter les requêtes excessives lors de la saisie. -
Reset pagination: Quand les filtres changent (level ou search), reset
pageà1automatiquement. -
"use client"obligatoire: La page utiliseuseStatepour les filtres → doit être Client Component. -
DB Session async: La route backend doit utiliser
AsyncSessioncomme les autres routes admin (voiradmin_routes.py). Si la DB est synchrone, adapter en conséquence. -
Pas de SQLAlchemy async si pas disponible: Vérifier si
get_dbretourne sync ou async session en regardantroutes/deps.py. Adapter l'implémentation backend.
📋 Checklist de Validation Avant Dev
- Vérifier si
get_db(dansroutes/deps.py) est synchrone ou asynchrone - Vérifier si
Translationmodel est importable depuisdatabase.models - Confirmer que
adminNavItemsdansconstants.tsinclut bien/admin/logs - Confirmer que les composants shadcn/ui
Table,Select,Inputsont 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
useQuerypour le data fetching,useMutationpour 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.tsxDOIT être en minuscules (sinon 404 Next.js)- Utiliser
API_BASEdepuis@/lib/configpour 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 actionfrontend/src/app/admin/system/DiskSpaceCard.tsx— pattern card metric
Fichiers existants à réutiliser/importer:
frontend/src/app/admin/useAdminDashboard.ts— pattern hook useQuery à suivrefrontend/src/app/admin/useTranslationStats.ts— patron exact pour mock data fallbackfrontend/src/app/admin/types.ts— types admin existants (ne pas redéfinir)frontend/src/app/admin/DateRangeFilter.tsx— exemple de composant filtrefrontend/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.tsxfrontend/src/app/admin/logs/useAdminLogs.tsfrontend/src/app/admin/logs/LogsTable.tsxfrontend/src/app/admin/logs/LogsFilters.tsxfrontend/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)