// 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', '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(); } // ── 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 { // 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; } }