aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-debug-supervisor.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-07-09 06:00:32 +0200
committerAgando <roduccomm@gmail.com>2026-07-09 06:00:32 +0200
commitcf7461ffeb4f53f3f50c9c692a2e4fdcecf17e82 (patch)
treee7780cd41271381cf108c9c81d71a3cb65cfd555 /modules/core/module-debug-supervisor.js
parentea4d09fb5ccba12ed774fcf7c913edfa1e6a851a (diff)
auto: 2026-07-09_06:00 changes
Diffstat (limited to 'modules/core/module-debug-supervisor.js')
-rw-r--r--modules/core/module-debug-supervisor.js701
1 files changed, 701 insertions, 0 deletions
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 <hash> — Deep analysis of a specific error',
+ 'debug-trends': 'debug-trends [days] — Error frequency over time (default: 7)',
+ 'debug-fix': 'debug-fix <hash> — 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 <hash> 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 <hash> for details', 'text-muted');
+ }
+
+ _cmdAnalyze(args) {
+ const hash = (args && args[0]) || '';
+ if (!hash) {
+ this.terminal.print('Usage: debug-analyze <hash>', '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 <hash>', '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;
+ }
+}