Files
boc/quixzoom-video-pipeline/monitoring/contradiction-alerts.js
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

299 lines
7.5 KiB
JavaScript

/**
* 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,
};