API réception alertes chute (SmartEye/YOLO), analyse IA (Gemini 2.5 Flash), gestion alertes avec escalade (watchdog), notifications Firebase, dashboard web, documentation MkDocs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
356 lines
18 KiB
PHP
Executable File
356 lines
18 KiB
PHP
Executable File
<?php
|
||
// --- 1. CONFIGURATION ---
|
||
$json_file = 'database.json';
|
||
$authorized_client = null;
|
||
$url_client = $_GET['client'] ?? '';
|
||
$url_token = $_GET['token'] ?? '';
|
||
$url_id = $_GET['id'] ?? null; // Support ID access
|
||
|
||
if (file_exists($json_file)) {
|
||
$data = json_decode(file_get_contents($json_file), true);
|
||
if (isset($data['clients'])) {
|
||
foreach ($data['clients'] as $id => $c) {
|
||
// Check 1: ID based (New)
|
||
if ($url_id !== null && (string)$id === (string)$url_id) {
|
||
$authorized_client = $c;
|
||
break;
|
||
}
|
||
// Check 2: Name/Token based (Legacy)
|
||
if (strcasecmp($c['name'], $url_client) == 0 && $c['token'] === $url_token) {
|
||
$authorized_client = $c;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!$authorized_client) { http_response_code(403); die("<h1>⛔ ACCÈS REFUSÉ</h1>"); }
|
||
|
||
// --- 2. ANALYSE DE L'ÉTAT ---
|
||
$client_folder = 'clients/' . $authorized_client['name'];
|
||
$images = is_dir($client_folder) ? glob($client_folder . "/*.jpg") : [];
|
||
array_multisort(array_map('filemtime', $images), SORT_DESC, $images);
|
||
|
||
// État par défaut (SAFE)
|
||
$state = 'SAFE';
|
||
$banner_color = '#10b981'; // Vert
|
||
$banner_text = '✅ SURVEILLANCE ACTIVE';
|
||
$status_text = 'R.A.S — Tout va bien';
|
||
$current_image = 'placeholder.jpg';
|
||
$alert_time = "--/--/----";
|
||
|
||
// DÉTECTION INTELLIGENTE
|
||
if (!empty($images)) {
|
||
$current_image = $images[0];
|
||
$alert_time = date("d/m/Y à H:i:s", filemtime($current_image));
|
||
|
||
// On demande à la Base de Données ce qu'en pense l'IA
|
||
$is_real_alert = $authorized_client['alerte'] ?? false;
|
||
|
||
// Lire le verdict RÉEL de l'IA depuis le sidecar JSON de l'image la plus récente
|
||
$sidecar_file = $current_image . ".json";
|
||
$sidecar_data = null;
|
||
$ia_was_urgent = null; // null = pas de données, true = IA a dit urgence, false = IA a dit RAS
|
||
if (file_exists($sidecar_file)) {
|
||
$sidecar_data = json_decode(file_get_contents($sidecar_file), true);
|
||
if ($sidecar_data && isset($sidecar_data['urgence'])) {
|
||
$ia_was_urgent = $sidecar_data['urgence'];
|
||
}
|
||
}
|
||
|
||
// SÉCURITÉ CRITIQUE : Détecter si la DB est en retard sur les alertes réelles
|
||
// Compare le timestamp de l'image vs la dernière mise à jour de la DB
|
||
$image_timestamp = filemtime($current_image);
|
||
$db_last_update = strtotime($authorized_client['last_update'] ?? '2000-01-01');
|
||
$db_is_stale = ($image_timestamp > $db_last_update + 60); // +60s de marge
|
||
|
||
// Est-ce que quelqu'un s'en occupe déjà ?
|
||
$handler = $authorized_client['handled_by'] ?? null;
|
||
$handler_time = $authorized_client['handled_at'] ?? '';
|
||
|
||
if ($handler) {
|
||
// ÉTAT ORANGE : PRIS EN CHARGE
|
||
$state = 'HANDLED';
|
||
$banner_color = '#f59e0b';
|
||
$banner_text = "🟠 PRISE EN CHARGE PAR " . strtoupper($handler);
|
||
$status_text = "⚠️ Intervention en cours par $handler depuis $handler_time";
|
||
}
|
||
elseif ($is_real_alert === true) {
|
||
// ÉTAT ROUGE : DB dit alerte active
|
||
$state = 'DANGER';
|
||
$banner_color = '#ef4444';
|
||
$banner_text = '🚨 ALERTE EN COURS';
|
||
$status_text = '⚠️ Chute Détectée — Intervention Requise';
|
||
}
|
||
elseif ($ia_was_urgent === true && $db_is_stale) {
|
||
// ÉTAT ROUGE : Le sidecar dit URGENCE mais la DB n'a pas été mise à jour
|
||
// → L'alerte n'a JAMAIS été traitée, la DB est en retard
|
||
$state = 'DANGER';
|
||
$banner_color = '#ef4444';
|
||
$banner_text = '🚨 ALERTE NON TRAITÉE';
|
||
$status_text = '⚠️ Alerte détectée — Aucune prise en charge enregistrée !';
|
||
}
|
||
elseif ($is_real_alert === false && $ia_was_urgent === true) {
|
||
// ÉTAT VERT : Alerte RÉELLE qui a été traitée et archivée
|
||
// La DB a été mise à jour APRÈS l'image → quelqu'un a géré l'incident
|
||
$state = 'RESOLVED';
|
||
$banner_color = '#10b981';
|
||
$banner_text = "✅ INCIDENT TRAITÉ & ARCHIVÉ";
|
||
$status_text = "L'alerte a été prise en charge et archivée.";
|
||
}
|
||
elseif ($is_real_alert === false) {
|
||
// FAUSSE ALERTE : L'IA a dit "pas d'urgence" (sidecar urgence=false ou absent)
|
||
$state = 'FALSE_POSITIVE';
|
||
$banner_color = '#3b82f6';
|
||
$banner_text = "🛡️ FAUSSE ALERTE ANNULÉE PAR IA";
|
||
$status_text = "ℹ️ Mouvement détecté, mais l'IA confirme que tout va bien.";
|
||
}
|
||
else {
|
||
// ÉTAT ROUGE : Fallback sécurité
|
||
$state = 'DANGER';
|
||
$banner_color = '#ef4444';
|
||
$banner_text = '🚨 ALERTE EN COURS';
|
||
$status_text = '⚠️ Chute Détectée — Intervention Requise';
|
||
}
|
||
}
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>LUCAS - Supervision</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
body { background-color: #0B1623; color: white; font-family: 'Roboto', sans-serif; margin: 0; display: flex; flex-direction: column; align-items: center; min-height: 100vh; }
|
||
header { width: 100%; padding: 15px; background: #0f1e31; display: flex; align-items: center; border-bottom: 1px solid #2c3e50; }
|
||
.status-dot { width: 12px; height: 12px; background: #10b981; border-radius: 50%; margin-right: 10px; box-shadow: 0 0 5px #10b981; }
|
||
.brand { font-weight: 700; color: #10b981; font-size: 1.2rem; }
|
||
.client-name { margin-left: auto; color: #94a3b8; font-size: 0.8rem; font-weight: bold; }
|
||
|
||
.alert-card { width: 100%; max-width: 600px; margin-top: 10px; }
|
||
.alert-banner { background: <?php echo $banner_color; ?>; padding: 12px; text-align: center; font-weight: bold; border-radius: 8px 8px 0 0; text-transform: uppercase; }
|
||
|
||
.image-wrapper { background: black; border-bottom: 4px solid #1e293b; }
|
||
.cam-image { width: 100%; display: block; }
|
||
|
||
.info-panel { background: #1e293b; padding: 20px; margin-top: 15px; border-radius: 8px; border: 1px solid #334155; }
|
||
.panel-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; border-bottom: 1px solid #334155; padding-bottom: 10px; }
|
||
.panel-time { font-size: 1.4rem; font-weight: bold; }
|
||
|
||
/* Couleur dynamique de la bordure IA */
|
||
<?php
|
||
$ai_color = '#fbbf24'; // Jaune par défaut
|
||
if ($state === 'DANGER') $ai_color = '#ef4444'; // Rouge
|
||
elseif ($state === 'FALSE_POSITIVE') $ai_color = '#3b82f6'; // Bleu
|
||
elseif ($state === 'RESOLVED') $ai_color = '#10b981'; // Vert
|
||
?>
|
||
.ai-report { background: #0f172a; border-left: 4px solid <?php echo $ai_color; ?>; padding: 15px; margin-bottom: 15px; border-radius: 4px; }
|
||
.ai-title { color: <?php echo $ai_color; ?>; font-weight: bold; font-size: 0.8rem; display: block; margin-bottom: 5px; }
|
||
.ai-text { color: #e2e8f0; font-size: 0.9rem; }
|
||
|
||
.action-zone { width: 100%; max-width: 560px; padding: 20px; }
|
||
select { width: 100%; padding: 15px; background: #1e293b; color: white; border: 1px solid #334155; border-radius: 8px; margin-bottom: 15px; font-size: 1rem; }
|
||
.btn { width: 100%; padding: 18px; border-radius: 12px; border: none; color: white; font-size: 1.1rem; font-weight: bold; cursor: pointer; display: flex; justify-content: center; align-items: center; text-decoration: none; margin-bottom: 10px; }
|
||
.btn-green { background: #10b981; }
|
||
.btn-blue { background: #3b82f6; }
|
||
.btn-red { background: #ef4444; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<div class="status-dot"></div><div class="brand">LUCAS</div>
|
||
<div class="client-name"><?php echo htmlspecialchars($authorized_client['name']); ?></div>
|
||
</header>
|
||
|
||
<div class="alert-card">
|
||
<div class="alert-banner"><?php echo $banner_text; ?></div>
|
||
|
||
<div class="image-wrapper">
|
||
<img src="<?php echo $current_image; ?>" class="cam-image">
|
||
</div>
|
||
|
||
<?php if ($state !== 'SAFE'): ?>
|
||
<div class="info-panel">
|
||
<div class="panel-row">
|
||
<div><span style="color:#94a3b8;font-size:0.8rem;">HORODATAGE</span><br><span class="panel-time"><?php echo $alert_time; ?></span></div>
|
||
<div style="font-size:1.5rem">🕒</div>
|
||
</div>
|
||
<div class="ai-report">
|
||
<span class="ai-title">🤖 ANALYSE IA <?php
|
||
if ($state === 'FALSE_POSITIVE') echo '(Rassurante)';
|
||
elseif ($state === 'RESOLVED') echo '(Archivée)';
|
||
elseif ($state === 'DANGER') echo '(ALERTE)';
|
||
?></span>
|
||
<div class="ai-text"><?php
|
||
// Priorité au sidecar (verdict réel de l'IA) si la DB est en retard
|
||
if ($db_is_stale && $sidecar_data && !empty($sidecar_data['message'])) {
|
||
echo htmlspecialchars($sidecar_data['message']);
|
||
} else {
|
||
echo htmlspecialchars($authorized_client['message'] ?? 'Analyse en cours...');
|
||
}
|
||
?></div>
|
||
</div>
|
||
<div style="text-align:center; font-weight:bold; color: <?php echo $banner_color; ?>;">
|
||
<?php echo $status_text; ?>
|
||
</div>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="info-panel" style="text-align:center;">
|
||
<h3 style="color:#10b981">Système Connecté</h3>
|
||
<p style="color:#94a3b8">Aucune anomalie détectée.</p>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- GALERIE HISTORIQUE -->
|
||
<?php if (!empty($images)): ?>
|
||
<style>
|
||
.history-title { margin-top: 30px; margin-bottom: 10px; color: #94a3b8; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 1px; font-weight: bold; padding-left: 5px; }
|
||
.history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 8px; max-height: 300px; overflow-y: auto; padding: 5px; background: #0f1e31; border-radius: 8px; border: 1px solid #2c3e50; }
|
||
.history-item { position: relative; cursor: pointer; border: 2px solid transparent; border-radius: 4px; overflow: hidden; transition: all 0.2s; aspect-ratio: 16/9; }
|
||
.history-item:hover { border-color: #3b82f6; transform: scale(1.05); z-index: 10; }
|
||
.history-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||
.history-item.active { border-color: #10b981; box-shadow: 0 0 8px rgba(16, 185, 129, 0.4); }
|
||
/* Scrollbar custom */
|
||
.history-grid::-webkit-scrollbar { width: 6px; }
|
||
.history-grid::-webkit-scrollbar-track { background: #0f172a; }
|
||
.history-grid::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
|
||
</style>
|
||
|
||
<div class="history-title">Historique des captures (<?php echo count($images); ?>)</div>
|
||
<div class="history-grid">
|
||
<?php foreach ($images as $idx => $img):
|
||
$timestamp = filemtime($img);
|
||
$date_str = date("d/m H:i", $timestamp);
|
||
|
||
// DATA ARCHIVED Check
|
||
$json_sidecar = $img . ".json";
|
||
$archived_data = null;
|
||
if (file_exists($json_sidecar)) {
|
||
$archived_data = json_decode(file_get_contents($json_sidecar), true);
|
||
}
|
||
|
||
// Prepare JS attributes
|
||
$safe_message = htmlspecialchars($archived_data['message'] ?? '', ENT_QUOTES);
|
||
$safe_urgence = $archived_data['urgence'] ?? null; // null if no archive
|
||
|
||
// If no archive, we pass 'null' to indicate "Unknown/Legacy"
|
||
$js_urgence = json_encode($safe_urgence);
|
||
?>
|
||
<div class="history-item <?php echo $idx === 0 ? 'active' : ''; ?>"
|
||
onclick='changeImage("<?php echo $img; ?>", "<?php echo date('d/m/Y à H:i:s', $timestamp); ?>", this, <?php echo $js_urgence; ?>, "<?php echo $safe_message; ?>")'>
|
||
<img src="<?php echo $img; ?>" loading="lazy" title="<?php echo $date_str; ?>">
|
||
<!-- Indicator for archived data -->
|
||
<?php if ($archived_data): ?>
|
||
<div style="position:absolute; bottom:2px; right:2px; width:8px; height:8px; border-radius:50%; background:<?php echo $safe_urgence ? '#ef4444' : '#3b82f6'; ?>;"></div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<script>
|
||
function changeImage(src, dateText, el, isUrgent, message) {
|
||
// Update Main Image
|
||
document.querySelector('.cam-image').src = src;
|
||
|
||
// Update Time
|
||
const timeEl = document.querySelector('.panel-time');
|
||
if(timeEl) timeEl.textContent = dateText;
|
||
|
||
// Update Active State
|
||
document.querySelectorAll('.history-item').forEach(d => d.classList.remove('active'));
|
||
el.classList.add('active');
|
||
|
||
// UPDATE AI REPORT CONTEXT
|
||
const aiReportBox = document.querySelector('.ai-report');
|
||
const aiTitle = document.querySelector('.ai-title');
|
||
const aiText = document.querySelector('.ai-text');
|
||
const banner = document.querySelector('.alert-banner');
|
||
const statusText = document.querySelector('.info-panel > div:last-child'); // The status text div
|
||
|
||
if (message) {
|
||
// We have archived data
|
||
aiText.textContent = message;
|
||
|
||
if (isUrgent === true) {
|
||
// Danger
|
||
if(aiReportBox) aiReportBox.style.borderLeftColor = '#ef4444';
|
||
if(aiTitle) { aiTitle.style.color = '#ef4444'; aiTitle.textContent = '🤖 ANALYSE IA (ARCHIVE)'; }
|
||
if(banner) { banner.style.backgroundColor = '#ef4444'; banner.textContent = '🚨 ALERTE (HISTORIQUE)'; }
|
||
if(statusText) { statusText.style.color = '#ef4444'; statusText.textContent = '⚠️ Alerte enregistrée à cette date'; }
|
||
} else {
|
||
// Safe / False Positive
|
||
const safeColor = '#3b82f6';
|
||
if(aiReportBox) aiReportBox.style.borderLeftColor = safeColor;
|
||
if(aiTitle) { aiTitle.style.color = safeColor; aiTitle.textContent = '🤖 ANALYSE IA (ARCHIVE)'; }
|
||
if(banner) { banner.style.backgroundColor = safeColor; banner.textContent = '🛡️ SÉCURISÉ (HISTORIQUE)'; }
|
||
if(statusText) { statusText.style.color = safeColor; statusText.textContent = 'ℹ️ Situation jugée sûre par l\'IA'; }
|
||
}
|
||
} else {
|
||
// Legacy image (no JSON)
|
||
if(aiText) aiText.textContent = "Aucune analyse archivée pour cette image.";
|
||
if(aiTitle) { aiTitle.style.color = '#94a3b8'; aiTitle.textContent = '🤖 ANALYSE IA (NON DISPONIBLE)'; }
|
||
if(aiReportBox) aiReportBox.style.borderLeftColor = '#94a3b8';
|
||
if(banner) { banner.style.backgroundColor = '#64748b'; banner.textContent = '🕰️ ARCHIVE SANS DONNÉES'; }
|
||
if(statusText) { statusText.style.color = '#94a3b8'; statusText.textContent = 'Pas de données d\'analyse pour cette date'; }
|
||
}
|
||
}
|
||
</script>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php if ($state !== 'SAFE'): ?>
|
||
<div class="action-zone">
|
||
|
||
<?php if ($state === 'DANGER'): ?>
|
||
<label style="color:#94a3b8; display:block; margin-bottom:5px;">Qui êtes-vous ?</label>
|
||
<select id="whoSelect">
|
||
<option value="" disabled selected>-- Choisir --</option>
|
||
<?php foreach (($authorized_client['contacts']??[]) as $c) echo "<option value='{$c['name']}'>{$c['name']}</option>"; ?>
|
||
</select>
|
||
<div style="display:flex; gap:10px;">
|
||
<a href="tel:112" class="btn btn-red" style="flex:1">📞 112</a>
|
||
<button class="btn btn-green" onclick="acknowledgeAlert()" style="flex:1">✅ Je m'en occupe</button>
|
||
</div>
|
||
|
||
<?php elseif ($state === 'FALSE_POSITIVE'): ?>
|
||
<p style="text-align:center; color:#94a3b8;">L'IA a invalidé l'alerte du capteur.</p>
|
||
<button class="btn btn-blue" onclick="finishIntervention()">🗑️ Archiver la fausse alerte</button>
|
||
|
||
<?php elseif ($state === 'RESOLVED'): ?>
|
||
<p style="text-align:center; color:#94a3b8;">Cet incident a été traité et archivé.</p>
|
||
<button class="btn btn-green" onclick="finishIntervention()">📂 Consulter l'historique</button>
|
||
|
||
<?php elseif ($state === 'HANDLED'): ?>
|
||
<p style="text-align:center; color:#94a3b8;">L'alerte est maintenue visible pour les autres.</p>
|
||
<button class="btn btn-blue" onclick="finishIntervention()">📂 Clore l'incident & Archiver</button>
|
||
<?php endif; ?>
|
||
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<script>
|
||
const params = new URLSearchParams(window.location.search);
|
||
const client = params.get('client');
|
||
const token = params.get('token');
|
||
|
||
function acknowledgeAlert() {
|
||
const user = document.getElementById('whoSelect').value;
|
||
if (!user) { alert("Dites-nous qui vous êtes !"); return; }
|
||
if(confirm("Confirmer la prise en charge ?")) {
|
||
fetch(`acknowledge.php?client=${client}&token=${token}&user=${encodeURIComponent(user)}`)
|
||
.then(r => r.json()).then(d => window.location.reload());
|
||
}
|
||
}
|
||
|
||
function finishIntervention() {
|
||
if(confirm("Confirmer l'archivage ?")) {
|
||
fetch(`reset.php?client=${client}&token=${token}&user=AutoArchive`)
|
||
.then(r => r.json()).then(d => window.location.reload());
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|