Files
boc/SIL/observability.mjs
T
Bernt 05ed037fe8 pilot.landvex.com: HTTPS + Full Stack Verified
- DNS: pilot.landvex.com -> 16.170.83.169
- TLS: Let's Encrypt certificate (expires 2026-09-30)
- Nginx: reverse proxy with SSL termination
- API: https://pilot.landvex.com/api/v1/missions
- UI: https://pilot.landvex.com/
- Upload: POST /api/v1/missions/import (multipart/form-data)

Verified:
 https://pilot.landvex.com/health
 https://pilot.landvex.com/version
 https://pilot.landvex.com/api/v1/missions (list)
 https://pilot.landvex.com/api/v1/missions/:id (get)
 POST /api/v1/missions/import (video upload)
 UI loads with title 'LandveX Intelligence Lab'

Next: Pilot 001 — Break the system!
2026-07-02 17:34:19 +00:00

442 lines
16 KiB
JavaScript

#!/usr/bin/env node
// ═══════════════════════════════════════════════════════════════════════════
// SIL Observability — Dashboard och metriker för SIL självt
// Erik-krav: "Bygg dashboards och mätvärden för SIL, inte bara för produkten"
// ═══════════════════════════════════════════════════════════════════════════
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
import { execSync } from 'child_process';
const LOGS = {
shadow: '/home/bernt/.openclaw/workspace/SIL/shadow-log.jsonl',
gold: '/home/bernt/.openclaw/workspace/SIL/gold-set.jsonl',
stability: '/home/bernt/.openclaw/workspace/SIL/stability-log.jsonl',
versions: '/home/bernt/.openclaw/workspace/SIL/graph-version-log.jsonl',
webhooks: '/home/bernt/.openclaw/workspace/SIL/webhook-events.jsonl',
decisions: '/home/bernt/.openclaw/workspace/SIL/decision-log.jsonl'
};
const DASHBOARD_DIR = '/home/bernt/.openclaw/workspace/SIL/dashboard';
class SILObservability {
constructor() {
if (!existsSync(DASHBOARD_DIR)) {
mkdirSync(DASHBOARD_DIR, { recursive: true });
}
}
loadLog(path) {
if (!existsSync(path)) return [];
return readFileSync(path, 'utf8')
.split('\n')
.filter(line => line.trim())
.map(line => {
try { return JSON.parse(line); } catch { return null; }
})
.filter(Boolean);
}
// ── Daglig sammanfattning ───────────────────────────────────────────────
generateDailyReport() {
const now = new Date();
const today = now.toISOString().split('T')[0];
const shadowEntries = this.loadLog(LOGS.shadow);
const goldEntries = this.loadLog(LOGS.gold);
const stabilityEntries = this.loadLog(LOGS.stability);
const versionEntries = this.loadLog(LOGS.versions);
const webhookEntries = this.loadLog(LOGS.webhooks);
// Filtrera dagens entries
const todayEntries = shadowEntries.filter(e =>
e.timestamp?.startsWith(today)
);
const report = {
date: today,
generatedAt: now.toISOString(),
// Beslut
decisions: {
total: todayEntries.length,
byType: this.countBy(todayEntries, 'status'),
laterDisproven: this.countDisproven(goldEntries, today)
},
// Osäkerhet
uncertainty: {
unknown: todayEntries.filter(e => e.uncertaintySummary?.unknown > 0).length,
uncertain: todayEntries.filter(e => e.uncertaintySummary?.uncertain > 0).length,
certain: todayEntries.filter(e => e.uncertaintySummary?.certain > 0).length
},
// Graf-hälsa
graph: {
versions: versionEntries.length,
latestVersion: versionEntries[versionEntries.length - 1]?.id || 'none',
nodes: versionEntries[versionEntries.length - 1]?.metadata?.nodeCount || 0,
edges: versionEntries[versionEntries.length - 1]?.metadata?.edgeCount || 0
},
// Stabilitet
stability: {
testsRun: stabilityEntries.length,
deterministic: stabilityEntries.filter(e =>
e.stability?.isDeterministic
).length,
avgStability: this.calculateAvgStability(stabilityEntries)
},
// Webhooks
webhooks: {
events: webhookEntries.filter(e =>
e.timestamp?.startsWith(today)
).length,
queued: webhookEntries.filter(e =>
e.status === 'queued' && e.timestamp?.startsWith(today)
).length,
completed: webhookEntries.filter(e =>
e.status === 'completed' && e.timestamp?.startsWith(today)
).length
},
// Kvalitetsmetriker över tid
quality: this.calculateQualityTrend(goldEntries)
};
this.saveDailyReport(report);
this.printDailyReport(report);
return report;
}
// ── Veckotrend ──────────────────────────────────────────────────────────
calculateQualityTrend(goldEntries) {
if (goldEntries.length === 0) return null;
// Gruppera per vecka
const byWeek = {};
for (const entry of goldEntries) {
const date = new Date(entry.date || entry.timestamp);
const week = this.getWeekKey(date);
if (!byWeek[week]) {
byWeek[week] = { entries: [], tp: 0, fp: 0, fn: 0 };
}
byWeek[week].entries.push(entry);
const predicted = new Set(entry.predicted || []);
const actual = new Set(entry.actual || []);
for (const comp of actual) {
if (predicted.has(comp)) byWeek[week].tp++;
else byWeek[week].fn++;
}
for (const comp of predicted) {
if (!actual.has(comp)) byWeek[week].fp++;
}
}
const trends = [];
for (const [week, data] of Object.entries(byWeek).sort()) {
const recall = data.tp + data.fn > 0 ? data.tp / (data.tp + data.fn) : 0;
const precision = data.tp + data.fp > 0 ? data.tp / (data.tp + data.fp) : 0;
trends.push({
week,
prs: data.entries.length,
recall: Math.round(recall * 100) + '%',
precision: Math.round(precision * 100) + '%',
fnr: data.tp + data.fn > 0 ? Math.round(data.fn / (data.tp + data.fn) * 100) + '%' : 'N/A'
});
}
return trends;
}
// ── Regelanvändning ─────────────────────────────────────────────────────
analyzeRuleUsage() {
const shadowEntries = this.loadLog(LOGS.shadow);
const ruleCounts = {};
const unusedRules = new Set([
'auth', 'wallet', 'mission', 'kyc', 'user',
'notification', 'api', 'database', 'tests', 'config', 'frontend', 'infrastructure'
]);
for (const entry of shadowEntries) {
const components = entry.components || entry.changedComponents || [];
if (!Array.isArray(components)) continue;
for (const comp of components) {
const compId = typeof comp === 'string' ? comp : comp.id;
if (compId) {
ruleCounts[compId] = (ruleCounts[compId] || 0) + 1;
unusedRules.delete(compId);
}
}
}
return {
used: Object.entries(ruleCounts)
.sort((a, b) => b[1] - a[1])
.map(([rule, count]) => ({ rule, count })),
unused: [...unusedRules],
totalRules: 12,
utilizationRate: Math.round((12 - unusedRules.size) / 12 * 100) + '%'
};
}
// ── Confidence-fördelning ───────────────────────────────────────────────
analyzeConfidence() {
const shadowEntries = this.loadLog(LOGS.shadow);
const distribution = {
'0.0-0.5': 0, '0.5-0.7': 0, '0.7-0.9': 0, '0.9-1.0': 0
};
for (const entry of shadowEntries) {
const conf = entry.confidence || 0;
if (conf >= 0.9) distribution['0.9-1.0']++;
else if (conf >= 0.7) distribution['0.7-0.9']++;
else if (conf >= 0.5) distribution['0.5-0.7']++;
else distribution['0.0-0.5']++;
}
return distribution;
}
// ── Helpers ─────────────────────────────────────────────────────────────
countBy(entries, key) {
const counts = {};
for (const entry of entries) {
const val = entry[key] || 'unknown';
counts[val] = (counts[val] || 0) + 1;
}
return counts;
}
countDisproven(goldEntries, date) {
// Räkna hur många beslut som senare visade sig felaktiga
let disproven = 0;
for (const entry of goldEntries) {
const predictions = entry.predictions || [];
const incorrect = predictions.filter(p => p.verified === false).length;
disproven += incorrect;
}
return disproven;
}
calculateAvgStability(entries) {
if (entries.length === 0) return 'N/A';
const scores = entries
.map(e => parseFloat(e.stability?.stabilityScore))
.filter(s => !isNaN(s));
if (scores.length === 0) return 'N/A';
return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) + '%';
}
getWeekKey(date) {
const d = new Date(date);
d.setHours(0, 0, 0, 0);
d.setDate(d.getDate() - d.getDay() + 1); // Måndag
return d.toISOString().split('T')[0];
}
// ── Output ──────────────────────────────────────────────────────────────
saveDailyReport(report) {
const filename = `${DASHBOARD_DIR}/daily-${report.date}.json`;
writeFileSync(filename, JSON.stringify(report, null, 2));
}
printDailyReport(report) {
console.log('╔═══════════════════════════════════════════════════════════════╗');
console.log('║ SIL OBSERVABILITY DASHBOARD ║');
console.log(`${report.date}`);
console.log('╚═══════════════════════════════════════════════════════════════╝');
console.log();
// Beslut
console.log('📊 BESLUT IDAG');
console.log();
console.log(` Totala analyser: ${report.decisions.total}`);
console.log(` Senare motbevisade: ${report.decisions.laterDisproven}`);
console.log();
// Osäkerhet
console.log('❓ OSÄKERHET');
console.log();
console.log(` UNKNOWN: ${report.uncertainty.unknown}`);
console.log(` UNCERTAIN: ${report.uncertainty.uncertain}`);
console.log(` CERTAIN: ${report.uncertainty.certain}`);
console.log();
// Graf
console.log('🕸️ GRAF-HÄLSA');
console.log();
console.log(` Versioner: ${report.graph.versions}`);
console.log(` Senaste: ${report.graph.latestVersion}`);
console.log(` Noder: ${report.graph.nodes}`);
console.log(` Kanter: ${report.graph.edges}`);
console.log();
// Stabilitet
console.log('🔄 STABILITET');
console.log();
console.log(` Tester körda: ${report.stability.testsRun}`);
console.log(` Deterministiska: ${report.stability.deterministic}`);
console.log(` Genomsnitt: ${report.stability.avgStability}`);
console.log();
// Webhooks
console.log('🌐 WEBHOOKS');
console.log();
console.log(` Events: ${report.webhooks.events}`);
console.log(` Köade: ${report.webhooks.queued}`);
console.log(` Klara: ${report.webhooks.completed}`);
console.log();
// Regelanvändning
const rules = this.analyzeRuleUsage();
console.log('📋 REGELANVÄNDNING');
console.log();
console.log(` Utilization: ${rules.utilizationRate}`);
console.log(` Oanvända: ${rules.unused.join(', ') || 'none'}`);
console.log();
// Confidence
const conf = this.analyzeConfidence();
console.log('🎯 CONFIDENCE-FÖRDELNING');
console.log();
for (const [range, count] of Object.entries(conf)) {
console.log(` ${range}: ${count}`);
}
console.log();
// Trend
if (report.quality) {
console.log('📈 KVALITETSTREND (veckovis)');
console.log();
for (const week of report.quality.slice(-4)) {
console.log(` ${week.week}: recall=${week.recall} precision=${week.precision} (${week.prs} PR:er)`);
}
console.log();
}
console.log('═══════════════════════════════════════════════════════════════');
console.log();
}
// ── HTML Dashboard ──────────────────────────────────────────────────────
generateHTMLDashboard() {
const report = this.generateDailyReport();
const html = `<!DOCTYPE html>
<html>
<head>
<title>SIL Dashboard — ${report.date}</title>
<style>
body { font-family: system-ui, sans-serif; margin: 40px; background: #f5f5f5; }
.card { background: white; border-radius: 8px; padding: 20px; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.metric { display: inline-block; margin: 10px 20px; }
.metric-value { font-size: 32px; font-weight: bold; color: #333; }
.metric-label { font-size: 14px; color: #666; }
.good { color: #22c55e; }
.warning { color: #f59e0b; }
.bad { color: #ef4444; }
h1 { color: #333; }
h2 { color: #555; border-bottom: 2px solid #eee; padding-bottom: 10px; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 10px; border-bottom: 1px solid #eee; }
th { color: #666; font-weight: 500; }
</style>
</head>
<body>
<h1>🔍 SIL Observability Dashboard</h1>
<p>${report.date} — Genererad ${report.generatedAt}</p>
<div class="card">
<h2>📊 Beslut idag</h2>
<div class="metric">
<div class="metric-value">${report.decisions.total}</div>
<div class="metric-label">Analyser</div>
</div>
<div class="metric">
<div class="metric-value ${report.decisions.laterDisproven > 0 ? 'bad' : 'good'}">${report.decisions.laterDisproven}</div>
<div class="metric-label">Motbevisade</div>
</div>
</div>
<div class="card">
<h2>❓ Osäkerhet</h2>
<div class="metric">
<div class="metric-value">${report.uncertainty.unknown}</div>
<div class="metric-label">UNKNOWN</div>
</div>
<div class="metric">
<div class="metric-value">${report.uncertainty.uncertain}</div>
<div class="metric-label">UNCERTAIN</div>
</div>
<div class="metric">
<div class="metric-value">${report.uncertainty.certain}</div>
<div class="metric-label">CERTAIN</div>
</div>
</div>
<div class="card">
<h2>🕸️ Graf-hälsa</h2>
<p>Version: <strong>${report.graph.latestVersion}</strong></p>
<p>Noder: ${report.graph.nodes} | Kanter: ${report.graph.edges}</p>
</div>
<div class="card">
<h2>📈 Kvalitetstrend</h2>
<table>
<tr><th>Vecka</th><th>PR:er</th><th>Recall</th><th>Precision</th><th>FNR</th></tr>
${(report.quality || []).map(w => `
<tr>
<td>${w.week}</td>
<td>${w.prs}</td>
<td>${w.recall}</td>
<td>${w.precision}</td>
<td>${w.fnr}</td>
</tr>
`).join('')}
</table>
</div>
</body>
</html>`;
const filename = `${DASHBOARD_DIR}/index.html`;
writeFileSync(filename, html);
console.log(`💾 HTML-dashboard sparad: ${filename}`);
}
}
// ── Main ──────────────────────────────────────────────────────────────────
const obs = new SILObservability();
const command = process.argv[2] || '--daily';
if (command === '--daily') {
obs.generateDailyReport();
} else if (command === '--html') {
obs.generateHTMLDashboard();
} else if (command === '--rules') {
const rules = obs.analyzeRuleUsage();
console.log(JSON.stringify(rules, null, 2));
} else {
console.log('Användning:');
console.log(' node observability.mjs --daily # Daglig rapport');
console.log(' node observability.mjs --html # Generera HTML-dashboard');
console.log(' node observability.mjs --rules # Analysera regelanvändning');
}