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>
465 lines
19 KiB
Markdown
465 lines
19 KiB
Markdown
# 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)
|