sentinel-core / script.js
hts-ai's picture
🐳 19/02 - 23:33 - je te laisse faire active de l'interieur
d14a41a verified
Raw
History Blame Contribute Delete
16.3 kB
// --- 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 = `<span class="opacity-50 mr-2">[${getTimestamp().split('T')[1].split('.')[0]}]</span> <i data-feather="${icon}" class="w-3 h-3 inline mr-1"></i> ${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 = `
<div class="flex justify-between items-center mb-1">
<strong class="text-amber-400">${safeArticleId}</strong>
<i data-feather="help-circle" class="w-3 h-3 text-amber-500"></i>
</div>
<p class="opacity-80">${safeType}</p>
<p class="opacity-50 text-[10px] mt-1 italic">${safeReason}</p>
`;
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 = `
<div class="absolute top-0 right-0 p-1"><i data-feather="x" class="w-3 h-3"></i></div>
<div class="flex justify-between items-center mb-1">
<strong class="text-red-400 font-bold tracking-wide">ALERTe</strong>
<span class="bg-red-600 text-white px-1 rounded text-[9px]">${safeType}</span>
</div>
<div class="mb-1 text-red-200 font-mono">${safeArticleId}</div>
<p class="opacity-80 border-t border-red-800/50 pt-1 mt-1">${safeReason}</p>
<p class="opacity-40 text-[9px] mt-2 font-mono">${getTimestamp()}</p>
`;
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 = '<div class="text-slate-500 italic">Système réinitialisé.</div>';
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;