# Proposition v2 — Tableau de bord futuriste pour `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** : fonctionnalités 100 % identiques (logique PHP, stats,
colonnes, échappement). Refonte esthétique complète en style **HUD futuriste**.

---

## 1. Concept visuel « HUD futuriste »

| Élément | Style |
|---|---|
| Fond | Espace profond avec **aurores animées** (dégradés cyan/violet flous en mouvement) + grille HUD |
| Typographie | **Orbitron** (titres, style sci-fi) + **JetBrains Mono** (données) via Google Fonts (autorisé par la CSP du site) |
| Cartes stats | **Verre dépoli** (glassmorphism) avec coins en équerre lumineux (style HUD) |
| Onglets | Segmented control néon, actif avec halo cyan |
| Tableau | Panneau verre, en-tête lumineux, **lignes de balayage** animées, hover avec lueur |
| Badges | Pastilles néon (cyan/rose/vert/ambre) avec glow |
| Animations | Pulsations subtiles, balayage, transitions douces — CSS pur, zéro JavaScript |

---

## 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>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
    <style>
        :root {
            --bg: #05060f;
            --panel: rgba(13, 16, 34, 0.72);
            --panel-2: rgba(20, 24, 48, 0.85);
            --border: rgba(90, 200, 255, 0.18);
            --border-glow: rgba(0, 229, 255, 0.45);
            --cyan: #00e5ff;
            --cyan-dim: #4dd8ff;
            --magenta: #ff2d95;
            --violet: #a78bfa;
            --green: #00ffa3;
            --amber: #ffb020;
            --red: #ff4d6d;
            --text: #d7e6ff;
            --text-dim: #7d8db3;
        }
        * { box-sizing: border-box; }
        body {
            margin: 0;
            font-family: "JetBrains Mono", "SF Mono", Consolas, monospace;
            background: var(--bg);
            color: var(--text);
            min-height: 100vh;
            padding: 24px;
            overflow-x: hidden;
        }

        /* ── Fond : aurores animées + grille HUD ── */
        .aurora {
            position: fixed; inset: 0; z-index: -2; overflow: hidden;
            background: radial-gradient(900px 500px at 15% -10%, rgba(0,229,255,0.10), transparent 60%),
                        radial-gradient(800px 500px at 85% 110%, rgba(255,45,149,0.08), transparent 60%),
                        radial-gradient(600px 400px at 60% 20%, rgba(167,139,250,0.07), transparent 60%);
        }
        .aurora::before, .aurora::after {
            content: ""; position: absolute; border-radius: 50%; filter: blur(90px); opacity: 0.5;
        }
        .aurora::before {
            width: 500px; height: 500px; left: -100px; top: -100px;
            background: radial-gradient(circle, rgba(0,229,255,0.25), transparent 70%);
            animation: drift 18s ease-in-out infinite alternate;
        }
        .aurora::after {
            width: 600px; height: 600px; right: -150px; bottom: -150px;
            background: radial-gradient(circle, rgba(255,45,149,0.18), transparent 70%);
            animation: drift 22s ease-in-out infinite alternate-reverse;
        }
        @keyframes drift {
            0%   { transform: translate(0, 0) scale(1); }
            100% { transform: translate(80px, 50px) scale(1.15); }
        }
        .grid-overlay {
            position: fixed; inset: 0; z-index: -1; pointer-events: none;
            background-image: linear-gradient(rgba(0,229,255,0.04) 1px, transparent 1px),
                              linear-gradient(90deg, rgba(0,229,255,0.04) 1px, transparent 1px);
            background-size: 44px 44px;
            mask-image: radial-gradient(ellipse at 50% 0%, black 30%, transparent 80%);
            -webkit-mask-image: radial-gradient(ellipse at 50% 0%, black 30%, transparent 80%);
        }

        .container { max-width: 1500px; margin: 0 auto; position: relative; }

        /* ── En-tête ── */
        .header {
            display: flex; align-items: center; justify-content: space-between;
            flex-wrap: wrap; gap: 14px; margin-bottom: 22px;
            padding: 18px 22px;
            background: var(--panel);
            border: 1px solid var(--border);
            border-radius: 14px;
            backdrop-filter: blur(10px);
            box-shadow: 0 0 30px rgba(0,229,255,0.06), inset 0 0 20px rgba(0,229,255,0.03);
            position: relative;
        }
        .header::before {
            content: ""; position: absolute; top: -1px; left: 24px; right: 24px; height: 1px;
            background: linear-gradient(90deg, transparent, var(--cyan), transparent);
            opacity: 0.6;
        }
        .header h1 {
            margin: 0; font-family: "Orbitron", sans-serif;
            font-size: 22px; font-weight: 900; letter-spacing: 3px;
            background: linear-gradient(90deg, var(--cyan), var(--violet));
            -webkit-background-clip: text; background-clip: text; color: transparent;
            text-shadow: 0 0 30px rgba(0,229,255,0.25);
        }
        .header .subtitle { color: var(--text-dim); font-size: 12px; margin-top: 6px; letter-spacing: 1px; }
        .header .date-badge {
            font-family: "Orbitron", sans-serif; font-size: 12px; letter-spacing: 2px;
            color: var(--cyan); padding: 8px 16px;
            border: 1px solid var(--border-glow); border-radius: 8px;
            background: rgba(0,229,255,0.05);
            box-shadow: 0 0 12px rgba(0,229,255,0.15), inset 0 0 10px rgba(0,229,255,0.05);
        }

        /* ── Onglets ── */
        .tabs { display: flex; gap: 10px; margin-bottom: 22px; flex-wrap: wrap; }
        .tabs a {
            display: inline-flex; align-items: center; gap: 9px;
            padding: 11px 20px; border-radius: 10px; text-decoration: none;
            font-family: "Orbitron", sans-serif; font-size: 12px; font-weight: 500; letter-spacing: 1.5px;
            background: var(--panel); border: 1px solid var(--border);
            color: var(--text-dim);
            backdrop-filter: blur(8px);
            transition: all 0.2s ease;
            position: relative;
        }
        .tabs a:hover { color: var(--cyan); border-color: var(--border-glow); box-shadow: 0 0 16px rgba(0,229,255,0.15); }
        .tabs a.active {
            color: #001018;
            background: linear-gradient(135deg, var(--cyan), #4dd8ff);
            border-color: var(--cyan);
            box-shadow: 0 0 24px rgba(0,229,255,0.4), inset 0 0 8px rgba(255,255,255,0.3);
            text-shadow: 0 1px 2px rgba(0,0,0,0.2);
        }

        /* ── Cartes stats ── */
        .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 14px; margin-bottom: 24px; }
        .stat-card {
            position: relative;
            background: var(--panel); border: 1px solid var(--border);
            border-radius: 12px; padding: 18px;
            backdrop-filter: blur(10px);
            overflow: hidden;
            transition: all 0.2s ease;
        }
        .stat-card:hover { border-color: var(--border-glow); box-shadow: 0 0 20px rgba(0,229,255,0.12); }
        .stat-card::before, .stat-card::after {
            content: ""; position: absolute; width: 14px; height: 14px; border: 2px solid var(--cyan); opacity: 0.7;
        }
        .stat-card::before { top: -1px; left: -1px; border-right: none; border-bottom: none; border-radius: 12px 0 0 0; }
        .stat-card::after { bottom: -1px; right: -1px; border-left: none; border-top: none; border-radius: 0 0 12px 0; }
        .stat-card .label {
            font-family: "Orbitron", sans-serif; font-size: 10px; letter-spacing: 2px;
            text-transform: uppercase; color: var(--text-dim);
        }
        .stat-card .value {
            font-size: 30px; font-weight: 600; margin-top: 8px;
            font-variant-numeric: tabular-nums; letter-spacing: 1px;
            text-shadow: 0 0 18px currentColor;
        }
        .stat-card .value.green { color: var(--green); }
        .stat-card .value.red { color: var(--red); }
        .stat-card .value.blue { color: var(--cyan); }
        .stat-card .value.accent { color: var(--violet); }

        /* ── Tableau ── */
        .table-wrap {
            position: relative;
            background: var(--panel); border: 1px solid var(--border);
            border-radius: 14px; overflow: auto; max-height: 72vh;
            backdrop-filter: blur(10px);
            box-shadow: 0 0 40px rgba(0,229,255,0.05);
        }
        .table-wrap::before {
            content: ""; position: absolute; top: 0; left: 0; right: 0; height: 1px;
            background: linear-gradient(90deg, transparent, var(--cyan), transparent);
            opacity: 0.5; z-index: 3;
        }
        table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
        thead th {
            position: sticky; top: 0; z-index: 2;
            background: var(--panel-2);
            color: var(--cyan-dim);
            text-align: left; padding: 13px 16px;
            font-family: "Orbitron", sans-serif; font-size: 10px;
            letter-spacing: 2px; text-transform: uppercase;
            border-bottom: 1px solid var(--border-glow);
            white-space: nowrap;
            text-shadow: 0 0 10px rgba(0,229,255,0.3);
        }
        tbody td { padding: 12px 16px; border-bottom: 1px solid rgba(90,200,255,0.07); vertical-align: top; }
        tbody tr { transition: background 0.15s ease; }
        tbody tr:nth-child(even) { background: rgba(0,229,255,0.02); }
        tbody tr:hover { background: rgba(0,229,255,0.07); box-shadow: inset 3px 0 0 var(--cyan); }
        tbody tr:last-child td { border-bottom: none; }

        /* ── Données ── */
        .ip { color: var(--red); font-weight: 600; white-space: nowrap; text-shadow: 0 0 12px rgba(255,77,109,0.35); }
        .ua { color: var(--amber); word-break: break-all; max-width: 320px; }
        .timestamp { color: var(--text-dim); white-space: nowrap; }
        .uri { color: var(--cyan-dim); word-break: break-all; max-width: 360px; }

        /* ── Badges néon ── */
        .badge {
            display: inline-block; padding: 4px 12px; border-radius: 20px;
            font-family: "Orbitron", sans-serif; font-size: 9.5px; font-weight: 500;
            letter-spacing: 1.5px; white-space: nowrap;
            border: 1px solid;
        }
        .badge.banned-3-days { color: var(--red); border-color: rgba(255,77,109,0.5); background: rgba(255,77,109,0.08); box-shadow: 0 0 12px rgba(255,77,109,0.2); }
        .badge.banned-redirect { color: var(--amber); border-color: rgba(255,176,32,0.5); background: rgba(255,176,32,0.08); box-shadow: 0 0 12px rgba(255,176,32,0.2); }
        .badge.logged { color: var(--text-dim); border-color: rgba(125,141,179,0.4); background: rgba(125,141,179,0.06); }
        .badge.sent { color: var(--green); border-color: rgba(0,255,163,0.5); background: rgba(0,255,163,0.08); box-shadow: 0 0 12px rgba(0,255,163,0.2); }
        .badge.failed { color: var(--red); border-color: rgba(255,77,109,0.5); background: rgba(255,77,109,0.08); box-shadow: 0 0 12px rgba(255,77,109,0.2); }
        .badge.code { color: var(--cyan); border-color: rgba(0,229,255,0.5); background: rgba(0,229,255,0.08); box-shadow: 0 0 12px rgba(0,229,255,0.2); }
        .badge.admin { color: var(--violet); border-color: rgba(167,139,250,0.5); background: rgba(167,139,250,0.08); box-shadow: 0 0 12px rgba(167,139,250,0.2); }
        .badge.confirmation { color: #c4a7e7; border-color: rgba(196,167,231,0.5); background: rgba(196,167,231,0.08); }

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

        /* ── Pied de page ── */
        .footer {
            margin-top: 16px; color: var(--text-dim); font-size: 11px; letter-spacing: 1px;
            display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px;
            padding: 0 4px;
        }
        .footer .pulse { color: var(--green); animation: blink 2s ease-in-out infinite; }
        @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }

        /* ── Scrollbar ── */
        ::-webkit-scrollbar { width: 10px; height: 10px; }
        ::-webkit-scrollbar-track { background: rgba(13,16,34,0.6); }
        ::-webkit-scrollbar-thumb { background: rgba(0,229,255,0.25); border-radius: 6px; }
        ::-webkit-scrollbar-thumb:hover { background: rgba(0,229,255,0.45); }

        @media (max-width: 640px) {
            body { padding: 14px; }
            .header h1 { font-size: 17px; letter-spacing: 2px; }
            .stat-card .value { font-size: 24px; }
        }
    </style>
</head>
<body>
    <div class="aurora"></div>
    <div class="grid-overlay"></div>

    <div class="container">
        <div class="header">
            <div>
                <h1>🪤 LOGS // RE-INFO</h1>
                <div class="subtitle">JOURNAL D'ACTIVITÉ — <?= htmlspecialchars(strtoupper($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</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(130px, 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"><?= htmlspecialchars($data['timestamp'] ?? 'N/A') ?></td>
                        <td class="ip"><?= 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><?= 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><?= 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"><?= htmlspecialchars(substr($data['user_agent'] ?? 'N/A', 0, 80)) ?></td>
                            <td class="uri"><?= 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><span class="pulse">●</span> SYSTÈME ACTIF — <?= $totalLines ?> LIGNE(S)</span>
            <span>SOURCE : <?= htmlspecialchars(strtoupper(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)
- Colonnes par type (bots / mails / submissions)
- Échappement `htmlspecialchars()` sur toutes les données
- Lignes d'erreur mail en `colspan=7`
- Ordre antéchronologique
- Aucun JavaScript ajouté (CSS pur)

## 4. Notes

- Google Fonts (Orbitron + JetBrains Mono) : **autorisés par la CSP du site**
  (`style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com`).
- Si les polices ne chargent pas, repli automatique sur monospace système.
- Le badge `BANNED_3_DAYS` garde son libellé (renommage = décision séparée).
