// --- Sentinel Logic Engine --- // State: Articles tracked by the system // Structure: { id, currentZone, paid, state (ok, latent, confirmed), lastSeen } let articles = {}; // State: Douts Latents (Set of Article IDs) let latentDoubts = new Set(); // State: Confirmed Alerts (Array of objects) let confirmedAlerts = []; // State: Statistics let resolvedDoubts = 0; let sessionStartTime = Date.now(); // DOM Elements - safely accessed function getElement(id) { return document.getElementById(id); } let eventLog, latentList, alertList, latentCount, alertCount, statTracked, statRate, statResolved, statTime; function initDOMElements() { eventLog = getElement('event-log'); latentList = getElement('latent-list'); alertList = getElement('alert-list'); latentCount = getElement('latent-count'); alertCount = getElement('alert-count'); statTracked = getElement('stat-tracked'); statRate = getElement('stat-rate'); statResolved = getElement('stat-resolved'); statTime = getElement('stat-time'); } // --- Helper Functions --- function getTimestamp() { return new Date().toISOString(); } function logEvent(message, type = 'info') { if (!eventLog) return; // Remove placeholder if exists const placeholder = document.getElementById('event-placeholder'); if (placeholder) placeholder.remove(); const entry = document.createElement('div'); let borderColor = 'border-slate-700'; let textColor = 'text-slate-300'; let icon = 'arrow-right'; let bgColor = 'bg-slate-800/50'; switch(type) { case 'doute': borderColor = 'border-amber-500'; textColor = 'text-amber-400'; icon = 'alert-triangle'; bgColor = 'bg-amber-900/20'; break; case 'alerte': borderColor = 'border-red-500'; textColor = 'text-red-400'; icon = 'alert-octagon'; bgColor = 'bg-red-900/20'; break; case 'validation': borderColor = 'border-green-500'; textColor = 'text-green-400'; icon = 'check-circle'; bgColor = 'bg-green-900/20'; break; case 'system': borderColor = 'border-cyan-500'; textColor = 'text-cyan-400'; icon = 'cpu'; bgColor = 'bg-cyan-900/20'; break; } entry.className = `p-2 ${bgColor} border-l-2 ${borderColor} ${textColor} text-[10px] animate-fade-in font-mono break-all mb-2 rounded-r`; entry.innerHTML = `[${getTimestamp().split('T')[1].split('.')[0]}] ${message}`; eventLog.prepend(entry); // Keep only last 100 entries while (eventLog.children.length > 100) { if (eventLog.lastChild) eventLog.lastChild.remove(); } if (typeof feather !== 'undefined' && typeof feather.replace === 'function') { feather.replace(); } updateStats(); } // --- Core Logic Implementation --- function processEvent(eventType, articleId) { // Sécurité cognitive : validation d'entrée if (!eventType || typeof eventType !== 'string') { console.error('[Sentinel Core] Invalid eventType:', eventType); return { error: 'Invalid eventType', etat: 'erreur' }; } if (!articleId || typeof articleId !== 'string') { console.error('[Sentinel Core] Invalid articleId:', articleId); return { error: 'Invalid articleId', etat: 'erreur' }; } const now = getTimestamp(); let output = { doute: null, raison: null, article: String(articleId), zone: null, timestamp: now, etat: "aucun" }; // 1. Initialization if new if (!articles[articleId]) { articles[articleId] = { id: String(articleId), currentZone: 'UNKNOWN', paid: false, state: 'ok', lastSeen: now }; } const article = articles[articleId]; // 2. Event Handling & Rules // CASE: Detection in Zone (Rayon, Réserve, etc.) if (eventType === 'RAYON') { output.zone = 'RAYON'; article.currentZone = 'RAYON'; article.lastSeen = now; // Reset latent if it was latent and comes back to authorized zone? // Usually stays latent until payment or exit, but let's assume tracking resumes normally. logEvent(`Détection article ${articleId} en zone RAYON`, 'info'); } else if (eventType === 'MOVE_RESERVE') { output.zone = 'RESERVE'; const previousZone = article.currentZone; article.currentZone = 'RESERVE'; article.lastSeen = now; // RULE: Déplacement non autorisé // Si l'article était en RAYON et passe en RESERVE (sans passer caisse logiquement dans ce scénario simplifié) if (previousZone === 'RAYON' && !article.paid) { const doubtType = "article_deplace"; createLatentDoubt(articleId, doubtType, `Article déplacé de ${previousZone} vers RESERVE sans validation.`); output.doute = doubtType; output.raison = "Déplacement suspect"; output.etat = "latent"; } else { logEvent(`Article ${articleId} déplacé vers RÉSERVE`, 'info'); } } else if (eventType === 'SCAN') { output.zone = 'CAISSE'; article.paid = true; article.currentZone = 'CAISSE'; // Technically leaving store logic-wise article.lastSeen = now; // Clear any latent doubts if paid if (latentDoubts.has(articleId)) { clearLatentDoubt(articleId); logEvent(`Validation article ${articleId} : Paiement confirmé`, 'validation'); } else { logEvent(`Scan article ${articleId} : OK`, 'validation'); } } else if (eventType === 'SORTIE') { output.zone = 'SORTIE'; article.currentZone = 'SORTIE'; article.lastSeen = now; // RULE: Sortie Client // Si article détecté en sortie ET article ∈ doutes_latents ET non payé if (latentDoubts.has(articleId) && !article.paid) { const doubtType = "article_non_declare_sortie_client"; confirmDoubt(articleId, doubtType, `Sortie zone non autorisée pour article en litige.`); output.doute = doubtType; output.raison = "Sortie non déclarée"; output.etat = "confirme"; } else if (!article.paid && article.state === 'ok' && article.currentZone === 'RAYON') { // Scénario: Article disparaît du rayon (non détecté déplacement) et sort direct. // On crée un doute immédiat à la sortie car l'état était ok mais non payé. const doubtType = "article_non_declare_sortie_client"; confirmDoubt(articleId, doubtType, `Sortie sans passage caisse détectée.`); output.doute = doubtType; output.raison = "Anomalie sortie"; output.etat = "confirme"; } else if (article.paid) { logEvent(`Sortie autorisée pour article ${articleId}`, 'validation'); } } updateMapVisualization(articleId, article); return output; } // --- Doubt Management --- function createLatentDoubt(articleId, type, reason) { // Validation défensive const safeArticleId = String(articleId || 'INCONNU'); const safeType = String(type || 'Type inconnu'); const safeReason = String(reason || 'Raison non spécifiée'); if (!latentDoubts.has(safeArticleId)) { latentDoubts.add(safeArticleId); if (articles[safeArticleId]) { articles[safeArticleId].state = 'latent'; } // UI Update avec sanitization const card = document.createElement('div'); card.id = `latent-${safeArticleId.replace(/[^a-zA-Z0-9-]/g, '')}`; card.className = "bg-amber-900/20 border border-amber-700/50 p-2 rounded text-xs text-amber-200 animate-fade-in"; card.innerHTML = `
${safeArticleId}

${safeType}

${safeReason}

`; if (latentList) latentList.appendChild(card); updateCounts(); logEvent(`DOUTE LATENT généré pour ${safeArticleId} : ${safeType}`, 'doute'); } } function clearLatentDoubt(articleId) { if (latentDoubts.has(articleId)) { latentDoubts.delete(articleId); articles[articleId].state = 'ok'; resolvedDoubts++; const el = document.getElementById(`latent-${articleId}`); if (el) { el.style.animation = 'fadeOut 0.3s ease-out forwards'; setTimeout(() => el.remove(), 300); } updateCounts(); updateStats(); } } function confirmDoubt(articleId, type, reason) { // Validation défensive et coercition de type const safeArticleId = String(articleId || 'INCONNU'); const safeType = String(type || 'Type inconnu'); const safeReason = String(reason || 'Raison non spécifiée'); // Remove from latent if exists clearLatentDoubt(safeArticleId); // Mark article (vérification d'existence) if (articles[safeArticleId]) { articles[safeArticleId].state = 'confirmed'; } // Add to confirmed confirmedAlerts.push({ article: safeArticleId, type: safeType, reason: safeReason, time: getTimestamp() }); // UI Update const card = document.createElement('div'); card.className = "bg-red-900/30 border border-red-600 p-2 rounded text-xs text-red-100 animate-fade-in alert-pulse relative overflow-hidden"; card.innerHTML = `
ALERTe ${safeType}
${safeArticleId}

${safeReason}

${getTimestamp()}

`; if (alertList) alertList.prepend(card); updateCounts(); logEvent(`ALERTE CONFIRMÉE pour ${safeArticleId} : ${safeType}`, 'alerte'); } function updateCounts() { if (latentCount) latentCount.innerText = latentDoubts.size; if (alertCount) alertCount.innerText = confirmedAlerts.length; } // --- Visualizer (2D Map) --- function updateMapVisualization(articleId, article) { if (!article) return; // Remove this specific dot from all zones first to avoid duplicates const existingDot = document.getElementById(`dot-visual-${articleId}`); if (existingDot) { existingDot.remove(); } // Create new dot with proper state class const dot = document.createElement('div'); dot.id = `dot-visual-${articleId}`; // Determine state class let stateClass = 'ok'; if (article.state === 'latent') stateClass = 'latent'; if (article.state === 'confirmed') stateClass = 'confirmed'; dot.className = `map-dot active ${stateClass}`; dot.style.width = '24px'; dot.style.height = '24px'; dot.style.display = 'flex'; dot.style.alignItems = 'center'; dot.style.justifyContent = 'center'; dot.style.fontSize = '10px'; dot.style.fontWeight = 'bold'; dot.style.color = 'white'; dot.innerText = articleId.split('-')[1] || '?'; // Determine target zone let targetZoneId = ''; switch(article.currentZone) { case 'RAYON': targetZoneId = 'zone-rayon-a'; break; case 'RESERVE': targetZoneId = 'zone-reserve'; break; case 'CAISSE': targetZoneId = 'zone-rayon-b'; break; case 'SORTIE': targetZoneId = 'zone-sortie'; break; default: targetZoneId = 'zone-entree'; } const targetZone = document.getElementById(targetZoneId); if (targetZone) { const dotContainer = targetZone.querySelector('[id^="dot-"]') || targetZone; if (dotContainer) { dotContainer.appendChild(dot); // Add entry animation dot.style.transform = 'scale(0)'; setTimeout(() => { dot.style.transform = 'scale(1)'; }, 50); } } } // Clean up dots for articles that no longer exist function cleanupMapDots() { const allDots = document.querySelectorAll('[id^="dot-visual-"]'); allDots.forEach(dot => { const articleId = dot.id.replace('dot-visual-', ''); if (!articles[articleId]) { dot.remove(); } }); } // --- Simulation Controller --- window.simulateEvent = function(type, id) { // Validation préventive pour éviter undefinednull if (!type || !id) { console.error('[Sentinel Core] Paramètres manquants:', { type, id }); logEvent(`Erreur: paramètres invalides (${type || 'undefined'}, ${id || 'null'})`, 'system'); return; } try { const result = processEvent(type, id); console.log("Agent Output:", result); // Flash effect on buttons could be added here } catch (err) { console.error('[Sentinel Core] Simulation error:', err); logEvent(`Erreur système interne: ${err.message}`, 'system'); } }; window.resetSystem = function() { articles = {}; latentDoubts.clear(); confirmedAlerts = []; resolvedDoubts = 0; sessionStartTime = Date.now(); if (eventLog) eventLog.innerHTML = '
Système réinitialisé.
'; if (latentList) latentList.innerHTML = ''; if (alertList) alertList.innerHTML = ''; document.querySelectorAll('.map-dot').forEach(el => el.remove()); updateCounts(); updateStats(); logEvent("Redémarrage du noyau Sentinel v1.0", 'system'); }; window.exportAlerts = function() { const data = { exportDate: getTimestamp(), totalAlerts: confirmedAlerts.length, resolvedDoubts: resolvedDoubts, alerts: confirmedAlerts }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `sentinel-alerts-${Date.now()}.json`; a.click(); URL.revokeObjectURL(url); logEvent("Export des alertes généré avec succès", 'system'); }; // Stats Update function updateStats() { if (!statTracked || !statRate || !statResolved || !statTime) return; const trackedCount = Object.keys(articles).length; statTracked.innerText = trackedCount; const totalDoubts = resolvedDoubts + latentDoubts.size + confirmedAlerts.length; const rate = totalDoubts > 0 ? Math.round((resolvedDoubts / totalDoubts) * 100) : 100; statRate.innerText = `${rate}%`; statResolved.innerText = resolvedDoubts; const elapsed = Math.floor((Date.now() - sessionStartTime) / 1000); const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0'); const seconds = (elapsed % 60).toString().padStart(2, '0'); statTime.innerText = `${minutes}:${seconds}`; } // Update timer every second setInterval(updateStats, 1000); // Cleanup map dots periodically setInterval(cleanupMapDots, 5000); // Init document.addEventListener('DOMContentLoaded', () => { initDOMElements(); logEvent("Initialisation du module Démarque Inconnue...", 'system'); logEvent("Connexion aux flux capteurs : OK", 'system'); logEvent("Moteur de détection prêt", 'system'); updateStats(); }); // Ensure all global functions are properly exposed window.simulateEvent = simulateEvent; window.resetSystem = resetSystem; window.exportAlerts = exportAlerts;