aboutsummaryrefslogtreecommitdiff
path: root/.bkp-fix-20261023_143429/module-debug-supervisor.js
diff options
context:
space:
mode:
Diffstat (limited to '.bkp-fix-20261023_143429/module-debug-supervisor.js')
-rw-r--r--.bkp-fix-20261023_143429/module-debug-supervisor.js889
1 files changed, 889 insertions, 0 deletions
diff --git a/.bkp-fix-20261023_143429/module-debug-supervisor.js b/.bkp-fix-20261023_143429/module-debug-supervisor.js
new file mode 100644
index 0000000..959d162
--- /dev/null
+++ b/.bkp-fix-20261023_143429/module-debug-supervisor.js
@@ -0,0 +1,889 @@
+// 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',
+ 'debug-sync': 'debug-sync [url] — Sync errors to backend API (or set sync URL)',
+ 'debug-sync-url': 'debug-sync-url [url] — View or set the backend sync endpoint',
+ };
+ }
+
+ 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();
+ this.commands['debug-sync'] = (args) => this._cmdSync(args);
+ this.commands['debug-sync-url'] = (args) => this._cmdSyncUrl(args);
+ }
+
+ seed() {
+ // seed() is not called automatically by module manager — interceptors go in init()
+ }
+
+ init() {
+ this._loadDB();
+ this._installInterceptors();
+ // Start periodic sync (every 5 minutes)
+ this._syncInterval = setInterval(() => this._syncToBackend(), 300000);
+ // Also sync on first load after a short delay
+ setTimeout(() => this._syncToBackend(), 5000);
+ }
+
+ destroy() {
+ // Clear sync interval
+ if (this._syncInterval) {
+ clearInterval(this._syncInterval);
+ this._syncInterval = null;
+ }
+ // Restore original console.error
+ if (this._originalConsole.error) {
+ console.error = this._originalConsole.error;
+ }
+ // Restore original window.onerror
+ if (this._originalOnError) {
+ window.onerror = this._originalOnError;
+ }
+ // Restore original fetch
+ if (this._originalFetch) {
+ window.fetch = this._originalFetch;
+ }
+ // Remove unhandledrejection listener (stored for cleanup)
+ if (this._rejectionHandler) {
+ window.removeEventListener('unhandledrejection', this._rejectionHandler);
+ }
+ this._interceptorInstalled = false;
+ this._healthy = false;
+ }
+
+ // ── 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 {
+ // Test localStorage is writable
+ 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' && /\b5\d{2}\b/.test(message)) 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;
+ this._rejectionHandler = 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);
+ };
+ window.addEventListener('unhandledrejection', this._rejectionHandler);
+
+ // Fetch failures
+ this._originalFetch = window.fetch;
+ window.fetch = function() {
+ const args = arguments;
+ const url = typeof args[0] === 'string' ? args[0] : (args[0]?.url || '?');
+ // Never record failures of the debug-sync endpoint itself: that produced a
+ // self-referential loop (38,495 identical "Fetch failed …/ingest.php" entries).
+ if (url.indexOf('/api/debug/') !== -1) return self._originalFetch.apply(window, args);
+ 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();
+
+ // v4.6.38 (A5): auto-file fatal/error-class failures, max 3 per day, deduped by hash.
+ if (classification.severity === 'fatal' || classification.severity === 'error') {
+ this._maybeAutoFile({ hash, message, source, type: classification.type, severity: classification.severity });
+ }
+ }
+
+ _maybeAutoFile(e) {
+ try { this._maybeAutoFileInner(e); } catch (_e) {}
+ }
+
+ _maybeAutoFileInner(e) {
+ try { if (localStorage.getItem('webx11_autoreport') === 'off') return; } catch (_e) {}
+
+ const today = new Date().toISOString().slice(0, 10);
+ let state = { day: today, count: 0, hashes: [] };
+ try {
+ const raw = JSON.parse(localStorage.getItem('webx11_autoreport_day') || 'null');
+ if (raw && raw.day === today && Array.isArray(raw.hashes)) state = raw;
+ } catch (_e) {}
+
+ if (state.count >= 3) return; // daily quota
+ if (state.hashes.indexOf(e.hash) !== -1) return; // one report per distinct error
+ state.count++;
+ state.hashes.push(e.hash);
+ try { localStorage.setItem('webx11_autoreport_day', JSON.stringify(state)); } catch (_e) {}
+
+ const payload = {
+ title: 'Auto: ' + String(e.message || 'unknown error').slice(0, 120),
+ body: 'Automatically filed by the debug supervisor (no user action).',
+ diagnostics: {
+ version: (typeof window !== 'undefined' && window.NEWOS4_VERSION) || 'unknown',
+ url: (typeof location !== 'undefined') ? location.href : '',
+ ua: (typeof navigator !== 'undefined') ? navigator.userAgent : '',
+ auto: true,
+ errors: [{
+ message: String(e.message || '').slice(0, 300),
+ source: String(e.source || '').slice(0, 200),
+ type: e.type,
+ severity: e.severity,
+ count: 1,
+ }],
+ },
+ };
+ try {
+ // /api/debug/* is excluded from the fetch interceptor, so a failure here cannot loop.
+ fetch('/spaces/newos4/api/debug/report.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ }).catch(function () {});
+ } catch (_e) {}
+ }
+
+ // ── 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 {
+ // Sanitize params: only allow alphanumeric, hyphens, underscores, dots
+ const safeParams = (firstFix.params || '').replace(/[^a-zA-Z0-9_\-\.]/g, '');
+ if (firstFix.action === 'loadmod' && this.terminal) {
+ this.terminal.executeCommand('loadmod ' + safeParams);
+ this.terminal.print(' ✅ Applied: loadmod ' + safeParams, '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');
+ }
+
+ // ── Sync to Backend ──
+
+ _getSyncUrl() {
+ try {
+ return localStorage.getItem('webx11_debug_sync_url') || 'https://webx11.duckdns.org/spaces/newos4/api/debug/ingest.php';
+ } catch (e) {
+ return 'https://webx11.duckdns.org/spaces/newos4/api/debug/ingest.php';
+ }
+ }
+
+ async _syncToBackend() {
+ if (!this._healthy) return;
+ const db = this._errorDB.errors;
+ const keys = Object.keys(db);
+ if (keys.length === 0) return;
+
+ const payload = {
+ version: window.NEWOS4_VERSION || 'v4.x',
+ session: {
+ id: this._sessionId,
+ modules: this.terminal?.moduleManager ? Object.keys(this.terminal.moduleManager.modules) : [],
+ },
+ errors: keys.map(k => ({
+ hash: k,
+ message: db[k].message,
+ type: db[k].type,
+ severity: db[k].severity,
+ category: db[k].category,
+ source: db[k].source,
+ module: db[k].module,
+ stack: db[k].stack,
+ count: db[k].count,
+ firstSeen: new Date(db[k].firstSeen).toISOString(),
+ lastSeen: new Date(db[k].lastSeen).toISOString(),
+ })),
+ };
+
+ try {
+ const resp = await fetch(this._getSyncUrl(), {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ signal: AbortSignal.timeout(10000),
+ });
+ if (resp.ok) {
+ const result = await resp.json();
+ if (result.success) {
+ const sent = keys.length;
+ const accepted = result.ingested || 0;
+ if (accepted < sent) {
+ console.warn('[DebugSupervisor] Sync truncated: sent ' + sent + ' errors, server accepted ' + accepted + ' (max ' + (result.total_errors || '?') + ' total)');
+ } else {
+ console.log('[DebugSupervisor] Synced ' + accepted + ' errors to backend');
+ }
+ this._lastSyncOk = Date.now();
+ return true;
+ }
+ }
+ console.warn('[DebugSupervisor] Sync returned HTTP ' + resp.status);
+ return false;
+ } catch (e) {
+ console.warn('[DebugSupervisor] Sync failed: ' + e.message);
+ return false;
+ }
+ }
+
+ _cmdSync(args) {
+ const url = (args && args[0]) ? args[0] : null;
+ if (url) {
+ try {
+ localStorage.setItem('webx11_debug_sync_url', url);
+ this.terminal.print('✅ Sync URL set to: ' + url, 'success');
+ } catch (e) {
+ this.terminal.print('❌ Failed to save sync URL', 'error');
+ }
+ return;
+ }
+ this.terminal.print('🔄 Syncing errors to backend...', 'info');
+ this._syncToBackend().then(ok => {
+ if (ok) {
+ this.terminal.print('✅ Sync complete', 'success');
+ } else {
+ this.terminal.print('❌ Sync failed', 'error');
+ }
+ });
+ }
+
+ _cmdSyncUrl(args) {
+ if (args && args[0]) {
+ try {
+ localStorage.setItem('webx11_debug_sync_url', args[0]);
+ this.terminal.print('✅ Sync URL set to: ' + args[0], 'success');
+ } catch (e) {
+ this.terminal.print('❌ Failed to save sync URL', 'error');
+ }
+ } else {
+ this.terminal.print('Current sync URL: ' + this._getSyncUrl(), 'info');
+ }
+ }
+
+ // ── 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;
+ }
+}