From 2538c91876455fe96febf441fc8dde20f2606023 Mon Sep 17 00:00:00 2001 From: Agando Date: Thu, 9 Jul 2026 06:00:32 +0200 Subject: auto: 2026-07-09_06:00 changes --- modules/core/module-debug-supervisor.js | 701 ++++++++++++++++++++++++++++++++ 1 file changed, 701 insertions(+) create mode 100644 modules/core/module-debug-supervisor.js (limited to 'modules/core/module-debug-supervisor.js') diff --git a/modules/core/module-debug-supervisor.js b/modules/core/module-debug-supervisor.js new file mode 100644 index 0000000..15fe5d4 --- /dev/null +++ b/modules/core/module-debug-supervisor.js @@ -0,0 +1,701 @@ +// module-debug-supervisor.js — Persistent error capture, clustering, root-cause analysis, and auto-fix +// Commands: debug-stats, debug-history, debug-analyze, debug-trends, debug-fix, debug-health +class DebugSupervisorModule { + constructor() { + this.commands = {}; + this.terminal = null; + this._healthy = true; + this._errorDB = { errors: {}, clusters: {}, patterns: {}, sessionId: null }; + this._maxErrors = 500; + this._ttlDays = 7; + this._writeDebounce = null; + this._pendingWrites = 0; + this._originalConsole = {}; + this._originalOnError = null; + this._originalOnRejection = null; + this._originalFetch = null; + this._interceptorInstalled = false; + this._sessionId = this._generateSessionId(); + } + + _generateSessionId() { + try { + let sid = localStorage.getItem('webx11_debug_session'); + if (!sid) { + sid = Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 8); + localStorage.setItem('webx11_debug_session', sid); + } + return sid; + } catch (e) { + return Date.now().toString(36); + } + } + + getHelpInfo() { + return { + 'debug-stats': 'debug-stats — Show error summary (total, unique, clusters, top issues)', + 'debug-history': 'debug-history [n] — Show last N errors (default: 20)', + 'debug-analyze': 'debug-analyze — Deep analysis of a specific error', + 'debug-trends': 'debug-trends [days] — Error frequency over time (default: 7)', + 'debug-fix': 'debug-fix — Suggest and optionally apply fix for an error', + 'debug-health': 'debug-health — Check supervisor module health status', + }; + } + + registerCommands() { + this.commands['debug-stats'] = () => this._cmdStats(); + this.commands['debug-history'] = (args) => this._cmdHistory(args); + this.commands['debug-analyze'] = (args) => this._cmdAnalyze(args); + this.commands['debug-trends'] = (args) => this._cmdTrends(args); + this.commands['debug-fix'] = (args) => this._cmdFix(args); + this.commands['debug-health'] = () => this._cmdHealth(); + } + + seed() { + // seed() is not called automatically by module manager — interceptors go in init() + } + + init() { + this._loadDB(); + this._installInterceptors(); + } + + // ── Error Database ── + + _loadDB() { + try { + const raw = localStorage.getItem('webx11_debug_db'); + if (raw) { + const parsed = JSON.parse(raw); + this._errorDB = parsed; + this._pruneExpired(); + } + } catch (e) { + // Corrupted DB — start fresh + this._errorDB = { errors: {}, clusters: {}, patterns: {}, sessionId: this._sessionId }; + } + } + + _saveDB() { + try { + localStorage.setItem('webx11_debug_db', JSON.stringify(this._errorDB)); + } catch (e) { + // Quota exceeded — evict oldest 20% and retry + this._evictLRU(0.2); + try { + localStorage.setItem('webx11_debug_db', JSON.stringify(this._errorDB)); + } catch (e2) { + // Still failing — supervisor degrades gracefully + console.warn('[DebugSupervisor] DB write failed, errors may be lost'); + } + } + } + + _scheduleSave() { + this._pendingWrites++; + if (this._writeDebounce) return; + this._writeDebounce = setTimeout(() => { + this._writeDebounce = null; + if (this._pendingWrites > 0) { + this._saveDB(); + this._pendingWrites = 0; + } + }, 2000); + } + + _pruneExpired() { + const now = Date.now(); + const maxAge = this._ttlDays * 86400000; + let changed = false; + for (const [hash, entry] of Object.entries(this._errorDB.errors)) { + if (now - entry.lastSeen > maxAge) { + delete this._errorDB.errors[hash]; + changed = true; + } + } + if (changed) this._saveDB(); + } + + _evictLRU(fraction) { + const entries = Object.entries(this._errorDB.errors); + const sorted = entries.sort((a, b) => a[1].lastSeen - b[1].lastSeen); + const toRemove = Math.ceil(sorted.length * fraction); + for (let i = 0; i < toRemove; i++) { + delete this._errorDB.errors[sorted[i][0]]; + } + } + + _checkQuota() { + try { + const total = 0; + for (const key in localStorage) { + // Estimate: key + value length + } + // Simple check: try writing a test key + const testKey = '__quota_test__'; + localStorage.setItem(testKey, '1'); + localStorage.removeItem(testKey); + return true; + } catch (e) { + return false; + } + } + + // ── Error Classification ── + + _classifyError(message, source, stack) { + const type = this._detectType(message, stack); + const severity = this._detectSeverity(type, message); + const category = this._detectCategory(message, source, type); + return { type, severity, category }; + } + + _detectType(message, stack) { + if (!message) return 'unknown'; + if (message.includes('ReferenceError') || message.includes('is not defined')) return 'ReferenceError'; + if (message.includes('TypeError') || message.includes('is not a function') || message.includes('cannot read property')) return 'TypeError'; + if (message.includes('SyntaxError') || message.includes('Unexpected token')) return 'SyntaxError'; + if (message.includes('HTTP') || message.includes('NetworkError') || message.includes('Failed to fetch')) return 'HTTP'; + if (message.includes('timeout') || message.includes('Timeout') || message.includes('AbortError')) return 'timeout'; + if (message.includes('QuotaExceededError') || message.includes('quota')) return 'quota'; + if (message.includes('SecurityError') || message.includes('permission')) return 'security'; + if (stack && stack.includes('tool_')) return 'tool'; + return 'runtime'; + } + + _detectSeverity(type, message) { + if (type === 'SyntaxError' || type === 'ReferenceError' || type === 'quota') return 'fatal'; + if (type === 'HTTP' && message.includes('5')) return 'error'; + if (type === 'timeout' || type === 'security') return 'error'; + if (type === 'tool') return 'warning'; + return 'error'; + } + + _detectCategory(message, source, type) { + if (type === 'HTTP' || type === 'timeout') return 'network'; + if (type === 'quota') return 'vfs'; + if (type === 'security') return 'security'; + if (source && (source.includes('module-') || source.includes('ext/'))) return 'module'; + if (type === 'tool') return 'tool'; + return 'runtime'; + } + + _extractModule(source) { + if (!source) return null; + const m = source.match(/(module-[a-z-]+)\.js/); + return m ? m[1] : null; + } + + _computeHash(message, source, type) { + const raw = (message || '') + '|' + (source || '') + '|' + (type || ''); + // Simple hash (Fowler-Noll-Vo) + let hash = 0x811c9dc5; + for (let i = 0; i < raw.length; i++) { + hash ^= raw.charCodeAt(i); + hash = Math.imul(hash, 0x01000193); + } + return (hash >>> 0).toString(16); + } + + // ── Error Interception ── + + _installInterceptors() { + if (this._interceptorInstalled) return; + const self = this; + + // Console.error + this._originalConsole.error = console.error; + console.error = function() { + const args = Array.prototype.slice.call(arguments); + const msg = args.map(a => typeof a === 'object' ? (a.message || JSON.stringify(a)) : String(a)).join(' '); + self._recordError(msg, 'console.error', null); + return self._originalConsole.error.apply(console, args); + }; + + // window.onerror + this._originalOnError = window.onerror; + window.onerror = function(msg, src, line, col, err) { + const fullMsg = msg + (err ? ' — ' + err.message : '') + ' (' + src + ':' + line + ':' + col + ')'; + self._recordError(fullMsg, src || 'window.onerror', err ? err.stack : null); + if (self._originalOnError) return self._originalOnError.call(window, msg, src, line, col, err); + }; + + // Unhandled rejections + this._originalOnRejection = window.onunhandledrejection; + window.addEventListener('unhandledrejection', function(e) { + const msg = e.reason ? (e.reason.message || String(e.reason)) : 'Unhandled Promise rejection'; + self._recordError(msg, 'unhandledrejection', e.reason ? e.reason.stack : null); + }); + + // Fetch failures + this._originalFetch = window.fetch; + window.fetch = function() { + const args = arguments; + const url = typeof args[0] === 'string' ? args[0] : (args[0]?.url || '?'); + return self._originalFetch.apply(window, args).catch(function(err) { + self._recordError('Fetch failed: ' + url + ' — ' + (err.message || err), url, err.stack); + throw err; + }); + }; + + this._interceptorInstalled = true; + } + + // ── Record Error ── + + recordToolError(toolName, args, errorMessage) { + this._recordError(errorMessage, 'tool:' + toolName, null); + } + + _recordError(message, source, stack) { + if (!this._healthy) return; + const classification = this._classifyError(message, source, stack); + const hash = this._computeHash(message, source, classification.type); + const now = Date.now(); + + if (this._errorDB.errors[hash]) { + this._errorDB.errors[hash].count++; + this._errorDB.errors[hash].lastSeen = now; + if (this._errorDB.errors[hash].sessions.indexOf(this._sessionId) === -1) { + this._errorDB.errors[hash].sessions.push(this._sessionId); + } + } else { + // LRU eviction if at cap + const keys = Object.keys(this._errorDB.errors); + if (keys.length >= this._maxErrors) { + this._evictLRU(0.1); + } + this._errorDB.errors[hash] = { + count: 1, + firstSeen: now, + lastSeen: now, + message: message, + type: classification.type, + severity: classification.severity, + category: classification.category, + source: source, + stack: stack ? stack.split('\n').slice(0, 10).join('\n') : null, + module: this._extractModule(source), + sessions: [this._sessionId], + }; + } + + this._scheduleSave(); + } + + // ── Clustering (Jaccard similarity) ── + + _normalizeMessage(msg) { + if (!msg) return ''; + return msg + .toLowerCase() + .replace(/\b0x[a-f0-9]+\b/g, '0xHEX') + .replace(/\b\d+\b/g, 'N') + .replace(/["'].*?["']/g, '"STR"') + .replace(/[^\w\s]/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + } + + _jaccardSimilarity(a, b) { + const setA = new Set(a.split(' ').filter(Boolean)); + const setB = new Set(b.split(' ').filter(Boolean)); + if (setA.size === 0 && setB.size === 0) return 1; + let intersection = 0; + for (const token of setA) { + if (setB.has(token)) intersection++; + } + const union = setA.size + setB.size - intersection; + return union === 0 ? 0 : intersection / union; + } + + _clusterErrors() { + const errors = Object.entries(this._errorDB.errors); + if (errors.length === 0) return []; + + const clusters = []; + const assigned = new Set(); + + for (let i = 0; i < errors.length; i++) { + if (assigned.has(i)) continue; + const [hashI, entryI] = errors[i]; + const normI = this._normalizeMessage(entryI.message); + const cluster = { hashes: [hashI], messages: [entryI.message], count: entryI.count, types: new Set([entryI.type]), categories: new Set([entryI.category]) }; + assigned.add(i); + + for (let j = i + 1; j < errors.length; j++) { + if (assigned.has(j)) continue; + const [hashJ, entryJ] = errors[j]; + const normJ = this._normalizeMessage(entryJ.message); + if (this._jaccardSimilarity(normI, normJ) >= 0.35) { + cluster.hashes.push(hashJ); + cluster.messages.push(entryJ.message); + cluster.count += entryJ.count; + cluster.types.add(entryJ.type); + cluster.categories.add(entryJ.category); + assigned.add(j); + } + } + + cluster.label = this._mostFrequent(cluster.messages); + cluster.typeCount = cluster.types.size; + cluster.categoryCount = cluster.categories.size; + delete cluster.types; + delete cluster.categories; + clusters.push(cluster); + } + + clusters.sort((a, b) => b.count - a.count); + return clusters; + } + + _mostFrequent(arr) { + const freq = {}; + let best = arr[0], bestCount = 1; + for (const item of arr) { + freq[item] = (freq[item] || 0) + 1; + if (freq[item] > bestCount) { + best = item; + bestCount = freq[item]; + } + } + return best; + } + + // ── Root-Cause Analysis ── + + _analyzeRootCause(hash) { + const entry = this._errorDB.errors[hash]; + if (!entry) return { found: false }; + + const findings = []; + const fixes = []; + + // Tool failure → check module + if (entry.source && entry.source.startsWith('tool:')) { + const toolName = entry.source.replace('tool:', ''); + const mm = this.terminal?.moduleManager; + if (mm) { + // Check if any module has this command + let found = false; + for (const [modName, mod] of Object.entries(mm.modules)) { + if (mod.commands && mod.commands[toolName]) { + found = true; + break; + } + } + if (!found) { + findings.push({ type: 'module_not_loaded', detail: 'No module provides command "' + toolName + '"' }); + fixes.push({ action: 'loadmod', params: toolName, description: 'Load module providing ' + toolName }); + } + } + } + + // HTTP error → check network + if (entry.type === 'HTTP' || entry.type === 'timeout') { + findings.push({ type: 'network_issue', detail: 'Network request failed: ' + entry.message.substring(0, 100) }); + fixes.push({ action: 'test-net', params: null, description: 'Run network diagnostics' }); + } + + // ReferenceError → check bundle + if (entry.type === 'ReferenceError') { + findings.push({ type: 'missing_class', detail: 'ReferenceError suggests a class or variable is not defined' }); + fixes.push({ action: 'reload', params: null, description: 'Reload the page to re-initialize all modules' }); + } + + // Module error → check module loader + if (entry.category === 'module') { + findings.push({ type: 'module_error', detail: 'Error in module: ' + (entry.module || 'unknown') }); + fixes.push({ action: 'loadmod', params: entry.module, description: 'Reload module: ' + entry.module }); + } + + return { found: findings.length > 0, findings, fixes }; + } + + // ── Fix Registry ── + + _buildFixSuggestion(hash) { + const entry = this._errorDB.errors[hash]; + if (!entry) return null; + + const rca = this._analyzeRootCause(hash); + if (!rca.found) { + return { confidence: 0, suggestion: 'No known fix pattern for this error', autoApply: false }; + } + + // Calculate confidence based on frequency + let confidence = Math.min(0.5, entry.count / 10); + if (entry.count >= 3) confidence = 0.7; + if (entry.count >= 10) confidence = 0.9; + + return { + confidence: confidence, + suggestion: rca.findings.map(f => f.detail).join('; '), + fixes: rca.fixes, + autoApply: confidence >= 0.9, + }; + } + + // ── Terminal Commands ── + + _cmdStats() { + const db = this._errorDB.errors; + const keys = Object.keys(db); + if (keys.length === 0) { + this.terminal.print('📊 Debug Stats: No errors recorded', 'success'); + return; + } + + const totalCount = keys.reduce((sum, k) => sum + db[k].count, 0); + const bySeverity = {}; + const byCategory = {}; + for (const k of keys) { + const e = db[k]; + bySeverity[e.severity] = (bySeverity[e.severity] || 0) + e.count; + byCategory[e.category] = (byCategory[e.category] || 0) + e.count; + } + + const clusters = this._clusterErrors(); + const topClusters = clusters.slice(0, 5); + + this.terminal.print('📊 Debug Stats', 'header'); + this.terminal.print(' Total errors: ' + totalCount, 'info'); + this.terminal.print(' Unique errors: ' + keys.length, 'info'); + this.terminal.print(' Clusters: ' + clusters.length, 'info'); + this.terminal.print(' Session: ' + this._sessionId, 'text-muted'); + this.terminal.print(' Supervisor: ' + (this._healthy ? '✅ healthy' : '🔴 degraded'), this._healthy ? 'success' : 'error'); + + this.terminal.print('── By Severity ──', 'info'); + for (const [sev, count] of Object.entries(bySeverity)) { + const icon = sev === 'fatal' ? '🔴' : sev === 'error' ? '🟡' : '🟢'; + this.terminal.print(' ' + icon + ' ' + sev + ': ' + count, sev === 'fatal' ? 'error' : 'warning'); + } + + this.terminal.print('── By Category ──', 'info'); + for (const [cat, count] of Object.entries(byCategory)) { + this.terminal.print(' • ' + cat + ': ' + count, 'text-muted'); + } + + if (topClusters.length > 0) { + this.terminal.print('── Top Clusters ──', 'info'); + for (const c of topClusters) { + this.terminal.print(' [' + c.hashes.length + ' errors ×' + c.count + '] ' + c.label.substring(0, 80), 'warning'); + } + this.terminal.print(' Run debug-analyze for details', 'text-muted'); + } + } + + _cmdHistory(args) { + const n = (args && args[0]) ? parseInt(args[0]) : 20; + const entries = Object.entries(this._errorDB.errors) + .sort((a, b) => b[1].lastSeen - a[1].lastSeen) + .slice(0, n); + + if (entries.length === 0) { + this.terminal.print('No errors recorded', 'success'); + return; + } + + this.terminal.print('── Error History (last ' + entries.length + ') ──', 'header'); + for (const [hash, e] of entries) { + const time = new Date(e.lastSeen).toLocaleTimeString(); + const icon = e.severity === 'fatal' ? '🔴' : e.severity === 'error' ? '🟡' : '🟢'; + const msg = e.message.substring(0, 80); + this.terminal.print(' ' + icon + ' [' + time + '] ' + msg, e.severity === 'fatal' ? 'error' : 'text-muted'); + this.terminal.print(' hash=' + hash + ' type=' + e.type + ' cat=' + e.category + ' ×' + e.count, 'text-muted'); + } + this.terminal.print('── End ──', 'info'); + this.terminal.print('Run debug-analyze for details', 'text-muted'); + } + + _cmdAnalyze(args) { + const hash = (args && args[0]) || ''; + if (!hash) { + this.terminal.print('Usage: debug-analyze ', 'warning'); + return; + } + + const entry = this._errorDB.errors[hash]; + if (!entry) { + this.terminal.print('Error not found: ' + hash, 'error'); + return; + } + + this.terminal.print('── Error Analysis: ' + hash + ' ──', 'header'); + this.terminal.print(' Message: ' + entry.message, 'info'); + this.terminal.print(' Type: ' + entry.type + ' | Severity: ' + entry.severity + ' | Category: ' + entry.category, 'info'); + this.terminal.print(' Source: ' + (entry.source || '?'), 'text-muted'); + this.terminal.print(' Module: ' + (entry.module || 'unknown'), 'text-muted'); + this.terminal.print(' Count: ' + entry.count + ' | First: ' + new Date(entry.firstSeen).toLocaleString() + ' | Last: ' + new Date(entry.lastSeen).toLocaleString(), 'info'); + this.terminal.print(' Sessions: ' + (entry.sessions || []).length, 'text-muted'); + if (entry.stack) { + this.terminal.print('── Stack Trace (top 10) ──', 'info'); + this.terminal.print(entry.stack, 'text-muted'); + } + + // Root-cause analysis + const rca = this._analyzeRootCause(hash); + if (rca.found) { + this.terminal.print('── Root Cause Analysis ──', 'warning'); + for (const f of rca.findings) { + this.terminal.print(' • [' + f.type + '] ' + f.detail, 'warning'); + } + if (rca.fixes.length > 0) { + this.terminal.print('── Suggested Fixes ──', 'success'); + for (const fix of rca.fixes) { + this.terminal.print(' → ' + fix.action + (fix.params ? ' ' + fix.params : '') + ' — ' + fix.description, 'success'); + } + } + } else { + this.terminal.print(' No root cause identified', 'text-muted'); + } + + // Fix suggestion + const fix = this._buildFixSuggestion(hash); + if (fix && fix.confidence > 0) { + this.terminal.print('── Auto-Fix Confidence: ' + Math.round(fix.confidence * 100) + '% ──', fix.autoApply ? 'success' : 'warning'); + this.terminal.print(' ' + fix.suggestion, 'info'); + if (fix.autoApply) { + this.terminal.print(' ⚡ Confidence ≥ 90% — fix can be auto-applied with debug-fix ' + hash, 'success'); + } + } + } + + _cmdTrends(args) { + const days = (args && args[0]) ? parseInt(args[0]) : 7; + const cutoff = Date.now() - days * 86400000; + + const entries = Object.values(this._errorDB.errors).filter(e => e.lastSeen >= cutoff); + if (entries.length === 0) { + this.terminal.print('No errors in the last ' + days + ' days', 'success'); + return; + } + + // Group by day + const byDay = {}; + for (const e of entries) { + const day = new Date(e.lastSeen).toISOString().split('T')[0]; + if (!byDay[day]) byDay[day] = { total: 0, byType: {} }; + byDay[day].total += e.count; + byDay[day].byType[e.type] = (byDay[day].byType[e.type] || 0) + e.count; + } + + const sortedDays = Object.keys(byDay).sort(); + this.terminal.print('── Error Trends (last ' + days + ' days) ──', 'header'); + for (const day of sortedDays) { + const d = byDay[day]; + const types = Object.entries(d.byType).map(([t, c]) => t + ':' + c).join(', '); + this.terminal.print(' ' + day + ': ' + d.total + ' errors (' + types + ')', d.total > 5 ? 'warning' : 'text-muted'); + } + + // Top error types + const typeTotals = {}; + for (const e of entries) { + typeTotals[e.type] = (typeTotals[e.type] || 0) + e.count; + } + this.terminal.print('── By Type ──', 'info'); + for (const [type, count] of Object.entries(typeTotals).sort((a, b) => b[1] - a[1])) { + this.terminal.print(' • ' + type + ': ' + count, 'text-muted'); + } + } + + _cmdFix(args) { + const hash = (args && args[0]) || ''; + if (!hash) { + this.terminal.print('Usage: debug-fix ', 'warning'); + return; + } + + const fix = this._buildFixSuggestion(hash); + if (!fix || fix.confidence === 0) { + this.terminal.print('No fix available for error: ' + hash, 'warning'); + return; + } + + this.terminal.print('── Fix Suggestion for ' + hash + ' ──', 'header'); + this.terminal.print(' Confidence: ' + Math.round(fix.confidence * 100) + '%', fix.autoApply ? 'success' : 'warning'); + this.terminal.print(' Suggestion: ' + fix.suggestion, 'info'); + + if (fix.fixes && fix.fixes.length > 0) { + this.terminal.print('── Available Fixes ──', 'info'); + for (const f of fix.fixes) { + this.terminal.print(' → ' + f.action + (f.params ? ' ' + f.params : ''), 'success'); + this.terminal.print(' ' + f.description, 'text-muted'); + } + } + + if (fix.autoApply) { + this.terminal.print('⚡ Auto-applying fix...', 'info'); + this._applyFix(hash, fix); + } else { + this.terminal.print('Confidence below auto-apply threshold. Run the suggested command manually.', 'warning'); + } + } + + _applyFix(hash, fix) { + if (!fix.fixes || fix.fixes.length === 0) return; + const firstFix = fix.fixes[0]; + try { + if (firstFix.action === 'loadmod' && this.terminal) { + this.terminal.executeCommand('loadmod ' + (firstFix.params || '')); + this.terminal.print(' ✅ Applied: loadmod ' + (firstFix.params || ''), 'success'); + } else if (firstFix.action === 'test-net' && this.terminal) { + this.terminal.executeCommand('test-net'); + this.terminal.print(' ✅ Applied: test-net', 'success'); + } else if (firstFix.action === 'reload') { + this.terminal.print(' ℹ️ Manual action needed: reload the page', 'warning'); + } else { + this.terminal.print(' ℹ️ Cannot auto-apply: ' + firstFix.action, 'warning'); + } + } catch (e) { + this.terminal.print(' ❌ Fix failed: ' + e.message, 'error'); + } + } + + _cmdHealth() { + this.terminal.print('── DebugSupervisor Health ──', 'header'); + this.terminal.print(' Status: ' + (this._healthy ? '✅ Healthy' : '🔴 Degraded'), this._healthy ? 'success' : 'error'); + this.terminal.print(' Session: ' + this._sessionId, 'text-muted'); + this.terminal.print(' Errors tracked: ' + Object.keys(this._errorDB.errors).length + ' unique', 'info'); + this.terminal.print(' Max errors: ' + this._maxErrors, 'text-muted'); + this.terminal.print(' TTL: ' + this._ttlDays + ' days', 'text-muted'); + this.terminal.print(' Interceptors: ' + (this._interceptorInstalled ? '✅ active' : '🔴 inactive'), this._interceptorInstalled ? 'success' : 'error'); + this.terminal.print(' Quota: ' + (this._checkQuota() ? '✅ available' : '🔴 low'), this._checkQuota() ? 'success' : 'error'); + } + + // ── Dashboard Data ── + + getDashboardData() { + const db = this._errorDB.errors; + const keys = Object.keys(db); + const totalCount = keys.reduce((sum, k) => sum + db[k].count, 0); + const clusters = this._clusterErrors().slice(0, 5); + + return { + totalUnique: keys.length, + totalCount: totalCount, + clusters: clusters.map(c => ({ label: c.label.substring(0, 60), count: c.count, hashes: c.hashes.length })), + healthy: this._healthy, + topType: this._getTopType(db), + topCategory: this._getTopCategory(db), + }; + } + + _getTopType(db) { + const freq = {}; + for (const e of Object.values(db)) { + freq[e.type] = (freq[e.type] || 0) + e.count; + } + const sorted = Object.entries(freq).sort((a, b) => b[1] - a[1]); + return sorted.length > 0 ? sorted[0][0] : null; + } + + _getTopCategory(db) { + const freq = {}; + for (const e of Object.values(db)) { + freq[e.category] = (freq[e.category] || 0) + e.count; + } + const sorted = Object.entries(freq).sort((a, b) => b[1] - a[1]); + return sorted.length > 0 ? sorted[0][0] : null; + } +} -- cgit v1.2.3