diff options
Diffstat (limited to 'modules/core/module-debug-supervisor.js')
| -rw-r--r-- | modules/core/module-debug-supervisor.js | 67 |
1 files changed, 65 insertions, 2 deletions
diff --git a/modules/core/module-debug-supervisor.js b/modules/core/module-debug-supervisor.js index 959d162..485f4e2 100644 --- a/modules/core/module-debug-supervisor.js +++ b/modules/core/module-debug-supervisor.js @@ -41,6 +41,8 @@ class DebugSupervisorModule { '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', + 'feedback': 'feedback <message> — Send feedback to the WebX11 maintenance agent', + 'feedback-inbox': 'feedback-inbox — Read replies from the maintenance agent (admin session)', }; } @@ -53,6 +55,8 @@ class DebugSupervisorModule { this.commands['debug-health'] = () => this._cmdHealth(); this.commands['debug-sync'] = (args) => this._cmdSync(args); this.commands['debug-sync-url'] = (args) => this._cmdSyncUrl(args); + this.commands['feedback'] = (args) => this._cmdFeedback(args); + this.commands['feedback-inbox'] = () => this._cmdFeedbackInbox(); } seed() { @@ -61,6 +65,9 @@ class DebugSupervisorModule { init() { this._loadDB(); + try { + this._syncedCounts = JSON.parse(localStorage.getItem('webx11_debug_synced') || '{}'); + } catch (_e) { this._syncedCounts = {}; } this._installInterceptors(); // Start periodic sync (every 5 minutes) this._syncInterval = setInterval(() => this._syncToBackend(), 300000); @@ -329,6 +336,12 @@ class DebugSupervisorModule { _maybeAutoFileInner(e) { try { if (localStorage.getItem('webx11_autoreport') === 'off') return; } catch (_e) {} + // Aborts/timeouts are the client's own cancellation, not application bugs, and the debug + // pipeline's own requests must never be filed as reports (self-referential loop). + // Measured 2026-10-21..23: 2 of 5 auto-filed reports were "user aborted a request". + if (/(user aborted|aborted without reason|signal timed out|AbortError|TimeoutError)/i.test(String(e.message || ''))) return; + if (/\/api\/debug\//.test(String(e.source || '')) || /\/api\/debug\//.test(String(e.message || ''))) return; + const today = new Date().toISOString().slice(0, 10); let state = { day: today, count: 0, hashes: [] }; try { @@ -764,7 +777,12 @@ class DebugSupervisorModule { async _syncToBackend() { if (!this._healthy) return; const db = this._errorDB.errors; - const keys = Object.keys(db); + const allKeys = Object.keys(db); + if (allKeys.length === 0) return; + // Only send hashes whose cumulative count changed since the last successful sync. + // The server counts deltas per (session, hash); re-sending unchanged rows is what + // multiplied one error into 46,133 occurrences before 2026-10-23. + const keys = allKeys.filter(k => (this._syncedCounts[k] || 0) !== db[k].count); if (keys.length === 0) return; const payload = { @@ -799,12 +817,14 @@ class DebugSupervisorModule { const result = await resp.json(); if (result.success) { const sent = keys.length; - const accepted = result.ingested || 0; + const accepted = result.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'); } + keys.forEach(k => { this._syncedCounts[k] = db[k].count; }); + try { localStorage.setItem('webx11_debug_synced', JSON.stringify(this._syncedCounts)); } catch (_e) {} this._lastSyncOk = Date.now(); return true; } @@ -817,6 +837,49 @@ class DebugSupervisorModule { } } + _feedbackThread() { + let t = null; + try { t = localStorage.getItem('webx11_feedback_thread'); } catch (_e) {} + if (!t) { + t = 'thr-' + Math.random().toString(36).slice(2, 10); + try { localStorage.setItem('webx11_feedback_thread', t); } catch (_e) {} + } + return t; + } + + _cmdFeedback(args) { + const msg = (Array.isArray(args) ? args.join(' ') : String(args || '')).trim(); + if (!msg) { this.terminal.print('usage: feedback <message>', 'error'); return; } + const payload = { + title: msg.slice(0, 120), + body: msg.slice(0, 4000), + thread: this._feedbackThread(), + kind: 'maintenance', + diagnostics: { version: window.NEWOS4_VERSION || '?', url: location.href, auto: false }, + }; + this.terminal.print('📨 Sending feedback…', ''); + fetch('/spaces/newos4/api/debug/report.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }).then(r => r.json()).then(j => { + if (j && j.success) this.terminal.print('✅ Sent: ' + (j.id || 'ok') + (j.duplicate ? ' (merged)' : ''), 'success'); + else this.terminal.print('❌ ' + ((j && j.error) || 'send failed'), 'error'); + }).catch(e => this.terminal.print('❌ ' + e.message, 'error')); + } + + _cmdFeedbackInbox() { + this.terminal.print('📬 Fetching replies…', ''); + fetch('/spaces/newos4/api/debug/agent-replies.php', { credentials: 'same-origin' }) + .then(r => r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status + ' (admin session required)'))) + .then(j => { + const list = (j && j.replies) || []; + if (!list.length) { this.terminal.print('(no replies yet)', ''); return; } + list.slice(-10).forEach(r => this.terminal.print('🤖 ' + (r.created_at || '') + ' ' + r.body, '')); + }) + .catch(e => this.terminal.print('❌ ' + e.message, 'error')); + } + _cmdSync(args) { const url = (args && args[0]) ? args[0] : null; if (url) { |