diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-23 14:39:30 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-23 14:39:30 +0200 |
| commit | 97f55bf200c74a1b8ca0068ff70cbfeda77916cf (patch) | |
| tree | f27fdfda0031d716bdc3717301eb2e250dbda9f8 /modules/core/module-debug-supervisor.js | |
| parent | 3bbaa9df177fcab2ee9dd04857d16d92c4812780 (diff) | |
fix(newos4-debug): idempotent ingest, admin-gated reads, honest counts (v4.6.52-53)
Root cause: _syncToBackend() re-sent the whole error DB with cumulative counts every
300s, and ingest.php did count += client count per batch. 33 real signatures had
accumulated 116,696 "occurrences" (one self-referential row alone: 46,133).
Backend
- ingest.php: per-(session,hash) synced map, adds only the delta, resets prev when the
client count shrank (cleared localStorage), caps delta at 5000, bounds synced at 500
hashes and sessions at 200, normalises timestamps to +00:00, 30-day retention that
also purges dropped hashes from every synced map, returns `accepted` (entries) next
to `ingested` (occurrences).
- stats.php/dashboard.php: total_errors = occurrences, unique_errors = signatures,
last-ingest card. The two numbers were inconsistent by ~3,500x.
- health.php: reports self_referential count and status=degraded instead of ok.
- Pruned live data: 33 -> 23 records, 116,696 -> 58,759 occurrences, max cluster
110,226 -> 52,387 (archive kept in data/debug/archive/).
- nginx: stats/errors/clusters/dashboard/agent-replies now behind
auth_request /_wx11_admin_auth (302 without a session); ingest/report/health stay
public for telemetry. Verified: reads 302, ingest 200, report 400.
Client
- module-debug-supervisor.js: sends only hashes whose count changed (persisted
webx11_debug_synced), records synced counts only after success, stops auto-filing
aborts/timeouts and debug-pipeline requests as bug reports (2 of the last 5 reports
were "user aborted a request").
Verified with a scratch php -S copy: same batch POSTed 3x -> count stays 5;
count=99999 -> clamped to 1000; retention dropped 10 legacy rows.
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) { |