aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-debug-supervisor.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core/module-debug-supervisor.js')
-rw-r--r--modules/core/module-debug-supervisor.js153
1 files changed, 143 insertions, 10 deletions
diff --git a/modules/core/module-debug-supervisor.js b/modules/core/module-debug-supervisor.js
index 15fe5d4..fc6a919 100644
--- a/modules/core/module-debug-supervisor.js
+++ b/modules/core/module-debug-supervisor.js
@@ -39,6 +39,8 @@ class DebugSupervisorModule {
'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',
};
}
@@ -49,6 +51,8 @@ class DebugSupervisorModule {
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() {
@@ -58,6 +62,36 @@ class DebugSupervisorModule {
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 ──
@@ -127,11 +161,7 @@ class DebugSupervisorModule {
_checkQuota() {
try {
- const total = 0;
- for (const key in localStorage) {
- // Estimate: key + value length
- }
- // Simple check: try writing a test key
+ // Test localStorage is writable
const testKey = '__quota_test__';
localStorage.setItem(testKey, '1');
localStorage.removeItem(testKey);
@@ -165,7 +195,7 @@ class DebugSupervisorModule {
_detectSeverity(type, message) {
if (type === 'SyntaxError' || type === 'ReferenceError' || type === 'quota') return 'fatal';
- if (type === 'HTTP' && message.includes('5')) return 'error';
+ 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';
@@ -222,10 +252,11 @@ class DebugSupervisorModule {
// Unhandled rejections
this._originalOnRejection = window.onunhandledrejection;
- window.addEventListener('unhandledrejection', function(e) {
+ 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;
@@ -636,9 +667,11 @@ class DebugSupervisorModule {
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 ' + (firstFix.params || ''));
- this.terminal.print(' ✅ Applied: loadmod ' + (firstFix.params || ''), 'success');
+ 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');
@@ -663,6 +696,106 @@ class DebugSupervisorModule {
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() {