/** * QUIXZOOM Video Pipeline — Contradiction Alert System * * Övervakar kontradiktioner i realtid och skickar varningar * när gapet mellan förväntad och observerad verklighet ökar. * * Konfigurerbara tröskelvärden: * - Low: 0-20 (grön) * - Moderate: 21-40 (gul) * - High: 41-60 (orange) * - Critical: 61-100 (röd) */ const Redis = require('ioredis'); const redis = new Redis({ host: process.env.REDIS_HOST || 'localhost', port: process.env.REDIS_PORT || 6379, }); /** * Alert-konfiguration */ const ALERT_CONFIG = { thresholds: { low: { min: 0, max: 20, color: 'green', action: 'none' }, moderate: { min: 21, max: 40, color: 'yellow', action: 'monitor' }, high: { min: 41, max: 60, color: 'orange', action: 'alert' }, critical: { min: 61, max: 100, color: 'red', action: 'critical' }, }, // Vilka kontradiktioner ska övervakas watchedContradictions: [ { id: 'commercial_vs_infrastructure', name: 'Commercial Vitality vs Infrastructure', description: 'High commercial activity but failing infrastructure', layers: ['commercial_vitality', 'infrastructure'], threshold: 25, // Poängdifferens som triggar varning }, { id: 'global_vs_informal', name: 'Global Presence vs Informal Economy', description: 'Multinationals operating alongside unregulated infrastructure', layers: ['economic', 'institutional'], threshold: 20, }, { id: 'physical_vs_operational', name: 'Physical Condition vs Activity Level', description: 'High activity in deteriorating physical environment', layers: ['physical', 'operational'], threshold: 20, }, { id: 'investment_vs_growth', name: 'Investment Confidence vs Growth', description: 'Growth without corresponding investment confidence', layers: ['investment_confidence', 'growth'], threshold: 15, }, ], // Notifieringskanaler notifications: { email: process.env.ALERT_EMAIL, slack: process.env.ALERT_SLACK_WEBHOOK, telegram: process.env.ALERT_TELEGRAM_BOT, }, // Cooldown mellan varningar (minuter) cooldownMinutes: 30, }; /** * Beräkna kontradiktion mellan två lager */ function calculateContradiction(score1, score2) { const diff = Math.abs(score1 - score2); const max = Math.max(score1, score2); // Normalisera till 0-100 return Math.min(100, (diff / max) * 100); } /** * Klassificera kontradiktionsnivå */ function classifyContradiction(index) { for (const [level, config] of Object.entries(ALERT_CONFIG.thresholds)) { if (index >= config.min && index <= config.max) { return { level, ...config }; } } return { level: 'unknown', color: 'gray', action: 'none' }; } /** * Kontrollera om varning ska skickas */ async function shouldSendAlert(contradictionId, level) { const key = `quixzoom:alert:cooldown:${contradictionId}`; const lastAlert = await redis.get(key); if (lastAlert) { const lastTime = new Date(lastAlert); const now = new Date(); const diffMinutes = (now - lastTime) / 1000 / 60; if (diffMinutes < ALERT_CONFIG.cooldownMinutes) { return false; } } // Sätt cooldown await redis.set(key, new Date().toISOString()); await redis.expire(key, ALERT_CONFIG.cooldownMinutes * 60); return true; } /** * Skicka varning */ async function sendAlert(alert) { console.log(`[ALERT] ${alert.level.toUpperCase()}: ${alert.title}`); console.log(`[ALERT] ${alert.description}`); console.log(`[ALERT] City: ${alert.city}, Score: ${alert.contradictionIndex}`); // Spara till Redis await redis.lpush('quixzoom:alerts', JSON.stringify({ ...alert, timestamp: new Date().toISOString(), })); // Behåll senaste 1000 varningar await redis.ltrim('quixzoom:alerts', 0, 999); // TODO: Skicka till notifieringskanaler // await sendEmail(alert); // await sendSlack(alert); // await sendTelegram(alert); } /** * Huvudfunktion — övervaka kontradiktioner */ async function monitorContradictions(city, scores) { const alerts = []; for (const config of ALERT_CONFIG.watchedContradictions) { const score1 = scores[config.layers[0]]; const score2 = scores[config.layers[1]]; if (score1 === undefined || score2 === undefined) continue; const index = calculateContradiction(score1, score2); const classification = classifyContradiction(index); // Kontrollera om tröskelvärdet är passerat const diff = Math.abs(score1 - score2); if (diff >= config.threshold) { const shouldAlert = await shouldSendAlert(config.id, classification.level); if (shouldAlert) { const alert = { id: config.id, title: config.name, description: config.description, city, contradictionIndex: Math.round(index), level: classification.level, color: classification.color, action: classification.action, scores: { [config.layers[0]]: score1, [config.layers[1]]: score2, difference: diff, }, }; await sendAlert(alert); alerts.push(alert); } } } return alerts; } /** * Hämta aktiva varningar */ async function getActiveAlerts(city = null, limit = 50) { const alerts = await redis.lrange('quixzoom:alerts', 0, limit - 1); return alerts .map(a => JSON.parse(a)) .filter(a => !city || a.city === city); } /** * Hämta varningsstatistik */ async function getAlertStats(city = null) { const alerts = await getActiveAlerts(city, 1000); const stats = { total: alerts.length, byLevel: {}, byType: {}, byCity: {}, timeline: {}, }; for (const alert of alerts) { // Per nivå stats.byLevel[alert.level] = (stats.byLevel[alert.level] || 0) + 1; // Per typ stats.byType[alert.id] = (stats.byType[alert.id] || 0) + 1; // Per stad stats.byCity[alert.city] = (stats.byCity[alert.city] || 0) + 1; // Timeline (per dag) const date = alert.timestamp.split('T')[0]; stats.timeline[date] = (stats.timeline[date] || 0) + 1; } return stats; } /** * Rensa gamla varningar */ async function cleanupOldAlerts(days = 30) { const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - days); const alerts = await redis.lrange('quixzoom:alerts', 0, -1); let removed = 0; for (let i = alerts.length - 1; i >= 0; i--) { const alert = JSON.parse(alerts[i]); if (new Date(alert.timestamp) < cutoff) { await redis.lrem('quixzoom:alerts', 0, alerts[i]); removed++; } } console.log(`[ALERT] Cleaned up ${removed} old alerts`); return removed; } /** * Exempel på användning */ async function example() { // Bangkok scores efter 5 videor const bangkokScores = { commercial_vitality: 85, infrastructure: 58, economic: 78, institutional: 61, physical: 62, operational: 84, investment_confidence: 71, growth: 82, }; const alerts = await monitorContradictions('Bangkok', bangkokScores); console.log(`[ALERT] Generated ${alerts.length} alerts`); // Hämta aktiva varningar const active = await getActiveAlerts('Bangkok'); console.log(`[ALERT] Active alerts: ${active.length}`); // Statistik const stats = await getAlertStats('Bangkok'); console.log('[ALERT] Stats:', stats); } // Kör exempel om filen körs direkt if (require.main === module) { example().catch(console.error); } module.exports = { calculateContradiction, classifyContradiction, monitorContradictions, getActiveAlerts, getAlertStats, cleanupOldAlerts, ALERT_CONFIG, };