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

465 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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)