# Proposition — Refonte esthétique de `admin/logs-viewer.php`

Date : 2026-08-16
Statut : **proposition, AUCUNE modification appliquée**.
Fichier concerné : `/home/alan/SITE/WEB/admin/logs-viewer.php`

**Règle respectée** : aucune fonctionnalité modifiée — la logique PHP (lecture
des logs, stats, tableaux, échappement `htmlspecialchars`) reste **strictement
identique**. Seuls le CSS et la structure HTML d'affichage changent.

---

## 1. Améliorations proposées

| # | Amélioration | Détail |
|---|---|---|
| 1 | **Thème sombre moderne** | Dégradé de fond, couleurs harmonisées (bleu/violet), plus de vert terminal brut |
| 2 | **Typographie** | Police système pour l'interface, `monospace` conservé pour les données (IP, UA, URI) |
| 3 | **Navigation en onglets** | Onglets arrondis avec état actif en surbrillance, compteurs sur chaque onglet |
| 4 | **Stats en cartes** | Statistiques dans des cartes colorées (Total, IPs uniques, Envoyés, Échecs, dernière heure) au lieu d'un paragraphe |
| 5 | **Tableau amélioré** | En-tête sticky, lignes zébrées, coins arrondis, hover plus doux, colonnes mieux proportionnées |
| 6 | **Badges d'action** | `BANNED_3_DAYS` (rouge), `BANNED_REDIRECT` (orange), `LOGGED` (gris), `sent` (vert), `failed` (rouge) — pastilles colorées lisibles |
| 7 | **Responsive** | Tableau défilable horizontalement sur mobile (wrapper `overflow-x`) |
| 8 | **En-tête de page** | Titre + sous-titre avec le type de log et la date du jour |
| 9 | **Pied de page** | Nombre de lignes affichées, rappel du fichier lu |
| 10 | **Scrollbar stylée** | Scrollbar sombre discrète (WebKit) |

---

## 2. Code proposé (remplacement complet du fichier)

```php
<?php
// Affichage des logs
$logFile = __DIR__ . '/../logs/trapped_bots.log';
$mailsLogFile = __DIR__ . '/../logs/contact_mails.log';
$submissionsLogFile = __DIR__ . '/../logs/contact_submissions.log';

$logType = $_GET['type'] ?? 'bots';
if ($logType === 'mails') {
    $currentFile = $mailsLogFile;
} elseif ($logType === 'submissions') {
    $currentFile = $submissionsLogFile;
} else {
    $logType = 'bots';
    $currentFile = $logFile;
}

if (!file_exists($currentFile)) {
    $labels = ['bots' => 'Aucun bot capturé pour le moment.', 'mails' => 'Aucun envoi d\'email enregistré pour le moment.', 'submissions' => 'Aucune soumission du formulaire enregistrée pour le moment.'];
    die($labels[$logType]);
}

$logs = file($currentFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$logs = array_reverse($logs); // Plus récents d'abord
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Logs — <?= htmlspecialchars($logType) ?></title>
    <style>
        :root {
            --bg: #0f1117;
            --bg-panel: #171a23;
            --bg-panel-2: #1d2130;
            --border: #2a2f42;
            --text: #e4e7ef;
            --text-dim: #8b93a7;
            --accent: #6c8cff;
            --accent-2: #a78bfa;
            --green: #34d399;
            --red: #f87171;
            --orange: #fbbf24;
            --blue: #60a5fa;
            --yellow: #fde047;
        }
        * { box-sizing: border-box; }
        body {
            margin: 0;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            background: radial-gradient(1200px 600px at 20% -10%, #1a2035 0%, var(--bg) 55%);
            color: var(--text);
            min-height: 100vh;
            padding: 24px;
        }
        .container { max-width: 1400px; margin: 0 auto; }

        /* En-tête */
        .header {
            display: flex; align-items: center; justify-content: space-between;
            flex-wrap: wrap; gap: 12px; margin-bottom: 20px;
        }
        .header h1 { margin: 0; font-size: 24px; font-weight: 700; letter-spacing: -0.3px; }
        .header h1 .dot { color: var(--accent); }
        .header .subtitle { color: var(--text-dim); font-size: 13px; margin-top: 4px; }
        .header .date-badge {
            background: var(--bg-panel-2); border: 1px solid var(--border);
            padding: 8px 14px; border-radius: 8px; font-size: 13px; color: var(--text-dim);
        }

        /* Onglets */
        .tabs { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; }
        .tabs a {
            display: inline-flex; align-items: center; gap: 8px;
            padding: 10px 18px; border-radius: 10px; text-decoration: none;
            background: var(--bg-panel); border: 1px solid var(--border);
            color: var(--text-dim); font-size: 14px; font-weight: 500;
            transition: all 0.15s ease;
        }
        .tabs a:hover { border-color: var(--accent); color: var(--text); }
        .tabs a.active {
            background: linear-gradient(135deg, #2b3a6e, #3b2f6e);
            border-color: var(--accent); color: #fff; box-shadow: 0 4px 14px rgba(108, 140, 255, 0.15);
        }
        .tabs a .count {
            background: rgba(255,255,255,0.08); border-radius: 20px;
            padding: 1px 9px; font-size: 12px; font-weight: 600;
        }
        .tabs a.active .count { background: rgba(255,255,255,0.18); }

        /* Cartes stats */
        .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px; }
        .stat-card {
            background: var(--bg-panel); border: 1px solid var(--border);
            border-radius: 12px; padding: 16px;
        }
        .stat-card .label { font-size: 12px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text-dim); }
        .stat-card .value { font-size: 26px; font-weight: 700; margin-top: 6px; font-variant-numeric: tabular-nums; }
        .stat-card .value.green { color: var(--green); }
        .stat-card .value.red { color: var(--red); }
        .stat-card .value.blue { color: var(--blue); }
        .stat-card .value.accent { color: var(--accent); }

        /* Tableau */
        .table-wrap {
            background: var(--bg-panel); border: 1px solid var(--border);
            border-radius: 14px; overflow-x: auto; overflow-y: auto; max-height: 70vh;
        }
        table { width: 100%; border-collapse: collapse; font-size: 13px; }
        thead th {
            position: sticky; top: 0; z-index: 2;
            background: var(--bg-panel-2); color: var(--text-dim);
            text-align: left; padding: 12px 14px; font-size: 11px;
            text-transform: uppercase; letter-spacing: 0.8px;
            border-bottom: 1px solid var(--border); white-space: nowrap;
        }
        tbody td { padding: 11px 14px; border-bottom: 1px solid #1c2030; vertical-align: top; }
        tbody tr:nth-child(even) { background: rgba(255,255,255,0.015); }
        tbody tr:hover { background: rgba(108, 140, 255, 0.06); }
        tbody tr:last-child td { border-bottom: none; }

        /* Données */
        .mono { font-family: "SF Mono", "Cascadia Code", Consolas, "Courier New", monospace; }
        .ip { color: var(--red); font-weight: 600; white-space: nowrap; }
        .ua { color: var(--yellow); word-break: break-all; max-width: 320px; }
        .timestamp { color: var(--text-dim); white-space: nowrap; }
        .uri { color: var(--blue); word-break: break-all; max-width: 360px; }

        /* Badges */
        .badge {
            display: inline-block; padding: 3px 10px; border-radius: 20px;
            font-size: 11px; font-weight: 600; letter-spacing: 0.3px; white-space: nowrap;
        }
        .badge.banned-3-days { background: rgba(248,113,113,0.15); color: var(--red); border: 1px solid rgba(248,113,113,0.3); }
        .badge.banned-redirect { background: rgba(251,191,36,0.12); color: var(--orange); border: 1px solid rgba(251,191,36,0.3); }
        .badge.logged { background: rgba(139,147,167,0.12); color: var(--text-dim); border: 1px solid rgba(139,147,167,0.3); }
        .badge.sent { background: rgba(52,211,153,0.12); color: var(--green); border: 1px solid rgba(52,211,153,0.3); }
        .badge.failed { background: rgba(248,113,113,0.15); color: var(--red); border: 1px solid rgba(248,113,113,0.3); }
        .badge.code { background: rgba(96,165,250,0.12); color: var(--blue); border: 1px solid rgba(96,165,250,0.3); }
        .badge.admin { background: rgba(167,139,250,0.12); color: var(--accent-2); border: 1px solid rgba(167,139,250,0.3); }
        .badge.confirmation { background: rgba(196,167,231,0.12); color: #c4a7e7; border: 1px solid rgba(196,167,231,0.3); }

        .error-detail { color: #ff9f43; font-size: 12px; word-break: break-all; }

        /* Pied de page */
        .footer { margin-top: 16px; color: var(--text-dim); font-size: 12px; display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px; }

        /* Scrollbar */
        ::-webkit-scrollbar { width: 10px; height: 10px; }
        ::-webkit-scrollbar-track { background: var(--bg-panel); }
        ::-webkit-scrollbar-thumb { background: #2f3650; border-radius: 6px; }
        ::-webkit-scrollbar-thumb:hover { background: #3d4566; }

        @media (max-width: 640px) {
            body { padding: 14px; }
            .header h1 { font-size: 20px; }
            .stat-card .value { font-size: 22px; }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <div>
                <h1>🪤 Logs <span class="dot">Re-info.fr</span></h1>
                <div class="subtitle">Journal d'activité — <?= htmlspecialchars($logType) ?></div>
            </div>
            <div class="date-badge">📅 <?= date('d/m/Y H:i') ?></div>
        </div>

        <nav class="tabs">
            <a href="logs-viewer.php?type=bots" class="<?= $logType === 'bots' ? 'active' : '' ?>">🤖 Bots capturés</a>
            <a href="logs-viewer.php?type=mails" class="<?= $logType === 'mails' ? 'active' : '' ?>">✉️ Emails envoyés</a>
            <a href="logs-viewer.php?type=submissions" class="<?= $logType === 'submissions' ? 'active' : '' ?>">📋 Soumissions formulaire</a>
        </nav>

        <div class="stats">
            <?php
            $totalLines = count($logs);
            $uniqueIps = [];
            $sentCount = 0;
            $failedCount = 0;
            $byType = [];
            $lastHour = 0;
            foreach ($logs as $log) {
                $data = json_decode($log, true);
                if (!$data) continue;
                if (!empty($data['ip'])) $uniqueIps[$data['ip']] = true;
                if (isset($data['status'])) {
                    if ($data['status'] === 'sent') $sentCount++;
                    if ($data['status'] === 'failed') $failedCount++;
                    if (isset($data['type'])) $byType[$data['type']] = ($byType[$data['type']] ?? 0) + 1;
                    if (!empty($data['unix_time']) && time() - $data['unix_time'] < 3600) $lastHour++;
                }
            }
            ?>
            <div class="stat-card">
                <div class="label">Total lignes</div>
                <div class="value accent"><?= $totalLines ?></div>
            </div>
            <div class="stat-card">
                <div class="label">IPs uniques</div>
                <div class="value blue"><?= count($uniqueIps) ?></div>
            </div>
            <?php if ($logType === 'mails'): ?>
                <div class="stat-card">
                    <div class="label">Envoyés</div>
                    <div class="value green"><?= $sentCount ?></div>
                </div>
                <div class="stat-card">
                    <div class="label">Échecs</div>
                    <div class="value red"><?= $failedCount ?></div>
                </div>
                <div class="stat-card">
                    <div class="label">Dernière heure</div>
                    <div class="value"><?= $lastHour ?></div>
                </div>
            <?php endif; ?>
        </div>

        <?php if ($logType === 'mails' && $byType): ?>
            <div class="stats" style="grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));">
                <?php foreach ($byType as $t => $n): ?>
                    <div class="stat-card">
                        <div class="label"><?= htmlspecialchars($t) ?></div>
                        <div class="value"><?= $n ?></div>
                    </div>
                <?php endforeach; ?>
            </div>
        <?php endif; ?>

        <div class="table-wrap">
            <table>
                <thead>
                    <tr>
                        <th>Date/Heure</th>
                        <th>IP</th>
                        <?php if ($logType === 'mails'): ?>
                            <th>Type</th>
                            <th>Destinataire</th>
                            <th>Sujet</th>
                            <th>Statut</th>
                            <th>Durée (s)</th>
                        <?php elseif ($logType === 'submissions'): ?>
                            <th>Événement</th>
                            <th>Résultat</th>
                            <th>Détails</th>
                        <?php else: ?>
                            <th>User-Agent</th>
                            <th>URI</th>
                            <th>Action</th>
                        <?php endif; ?>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($logs as $log):
                        $data = json_decode($log, true);
                        if (!$data) continue;
                    ?>
                    <tr>
                        <td class="timestamp mono"><?= htmlspecialchars($data['timestamp'] ?? 'N/A') ?></td>
                        <td class="ip mono"><?= htmlspecialchars($data['ip'] ?? 'N/A') ?></td>
                        <?php if ($logType === 'mails'): ?>
                            <td><span class="badge <?= htmlspecialchars($data['type'] ?? '') ?>"><?= htmlspecialchars($data['type'] ?? 'N/A') ?></span></td>
                            <td class="mono"><?= htmlspecialchars($data['to'] ?? 'N/A') ?></td>
                            <td><?= htmlspecialchars($data['subject'] ?? 'N/A') ?></td>
                            <td><span class="badge <?= ($data['status'] ?? '') === 'failed' ? 'failed' : 'sent' ?>"><?= htmlspecialchars($data['status'] ?? 'N/A') ?></span></td>
                            <td class="mono"><?= htmlspecialchars($data['duration'] ?? 'N/A') ?></td>
                            <?php if (!empty($data['error'])): ?>
                                </tr><tr><td colspan="7" class="error-detail">Erreur : <?= htmlspecialchars($data['error']) ?></td>
                            <?php endif; ?>
                        <?php elseif ($logType === 'submissions'): ?>
                            <td><?= htmlspecialchars($data['event'] ?? 'N/A') ?></td>
                            <td><?= htmlspecialchars($data['result'] ?? 'N/A') ?></td>
                            <td><?= htmlspecialchars($data['email'] ?? $data['remaining'] ?? '') ?></td>
                        <?php else: ?>
                            <td class="ua mono"><?= htmlspecialchars(substr($data['user_agent'] ?? 'N/A', 0, 80)) ?></td>
                            <td class="uri mono"><?= htmlspecialchars($data['uri'] ?? 'N/A') ?></td>
                            <td>
                                <?php
                                $action = $data['action_taken'] ?? 'LOGGED';
                                $badgeClass = 'logged';
                                if ($action === 'BANNED_3_DAYS') $badgeClass = 'banned-3-days';
                                elseif ($action === 'BANNED_REDIRECT') $badgeClass = 'banned-redirect';
                                ?>
                                <span class="badge <?= $badgeClass ?>"><?= htmlspecialchars($action) ?></span>
                            </td>
                        <?php endif; ?>
                    </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
        </div>

        <div class="footer">
            <span><?= $totalLines ?> ligne(s) affichée(s)</span>
            <span>Fichier : <?= htmlspecialchars(basename($currentFile)) ?></span>
        </div>
    </div>
</body>
</html>
```

---

## 3. Ce qui ne change PAS (fonctionnalités intactes)

- Lecture des 3 fichiers de log et sélection via `?type=`
- Calcul des stats (total, IPs uniques, sent/failed, dernière heure, répartition)
- Affichage des colonnes par type (bots / mails / submissions)
- Échappement `htmlspecialchars()` sur toutes les données
- Lignes d'erreur mail en `colspan=7`
- Ordre antéchronologique

## 4. Notes

- Le badge `BANNED_3_DAYS` garde son libellé d'origine (renommage éventuel en
  `BANNED_24H` = décision séparée, hors périmètre esthétique).
- Aucun JavaScript ajouté : tout est CSS pur, aucun risque de casse.
- Le fichier est dans `admin/` (protégé par le .htaccess du dossier logs ? non —
  `admin/` n'est pas protégé ; vérifier l'accès avant déploiement si besoin).
