Files
Lucas/photos.php
Debian 24dbc7cd6a Initial commit — Serveur Lucas SmartEye
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>
2026-03-14 21:26:06 +01:00

356 lines
18 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
<?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>