From e1cdf03d40de79ae8b55bb12ab448a9a61291a8e Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Fri, 21 Aug 2026 09:00:19 -0400 Subject: [PATCH] Say why mesh chat failed instead of retrying a minute later Every fetch ended in an empty catch, so a failed channel list was indistinguishable from a slow one: nothing rendered, nothing was logged, and the next attempt was a backoff tick away. Errors now land in the card's status line and the console, a failure retries in two seconds, and opening the pane before the list has arrived asks for it rather than waiting. --- Plugin/unraid/include/node_chat.php | 64 +++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/Plugin/unraid/include/node_chat.php b/Plugin/unraid/include/node_chat.php index d1b4e01..69489fc 100644 --- a/Plugin/unraid/include/node_chat.php +++ b/Plugin/unraid/include/node_chat.php @@ -438,11 +438,47 @@ function _vvNcWho(id) { return full ? (_vvNc.hosts[id] || id) : id.toUpperCase(); } +// Whatever went wrong, said out loud in the card's own status line. +// +// Every fetch here used to end in `.catch(() => {})`. A failed channel list was therefore +// indistinguishable from a slow one: nothing appeared, nothing was logged, and the next attempt +// was a backoff tick away — up to ninety seconds — so a single failed request looked exactly like +// "the chat takes a minute to load". Silence plus a long retry is how a fast failure becomes a +// slow mystery. +function _vvNcErr(where, e) { + _vvNcErrShown = true; + const st = document.getElementById('vv-nc-status'); + const msg = (e && e.message) ? e.message : String(e || 'failed'); + if (st) { st.textContent = where + ': ' + msg; st.style.color = '#a05a2c'; } + try { console.warn('[mesh chat] ' + where, e); } catch (_) {} +} +// Cleared on any success. Deliberately not conditional on "was it showing an error" — comparing a +// computed style back to the hex that set it is a string match on the browser's rgb() rendering, +// which is the kind of check that works until it silently does not. +let _vvNcErrShown = false; +function _vvNcOk() { + if (!_vvNcErrShown) return; + _vvNcErrShown = false; + const st = document.getElementById('vv-nc-status'); + if (st) { st.textContent = ''; st.style.color = '#3a3a3a'; } +} + +// A failed attempt retries in two seconds rather than waiting for the next backoff tick. Recovery +// should track how long the failure lasted, not how long the poller happens to be sleeping. +function _vvNcRetrySoon() { + if (_vvNcTimer) clearTimeout(_vvNcTimer); + _vvNcTimer = setTimeout(() => { vvNcChans(); _vvNcSchedule(); }, 2000); +} + function vvNcChans() { fetch('/plugins/varaverk/api/node_chat.php?_=' + Date.now()) - .then(r => r.json()) + .then(r => { + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.json(); + }) .then(d => { - if (!d.ok) return; + if (!d.ok) { _vvNcErr('channels', d.error || 'endpoint returned ok:false'); return; } + _vvNcOk(); _vvNc.me = d.me; _vvNc.chans = d.channels || []; _vvNc.hosts = d.hostnames || {}; const sel = document.getElementById('vv-nc-ch'); if (!sel) return; @@ -457,17 +493,27 @@ function vvNcChans() { const b = document.getElementById('vv-nc-badge'); if (b) { b.hidden = !tot || _vvNcMeshOn; b.textContent = '✉ ' + tot; } }) - .catch(() => {}); + .catch(e => { _vvNcErr('channels', e); _vvNcRetrySoon(); }); } function vvNcSelect(ch) { _vvNc.ch = ch; vvNcLoad(); } function vvNcLoad() { - if (!_vvNc.ch) return; + // No channel yet means the list has not landed — ask for it rather than returning silently and + // leaving the transcript on "loading" until a backoff tick happens to fetch it. This is the path + // taken when the mesh pane is opened before the first channel list has arrived. + if (!_vvNc.ch) { vvNcChans(); return; } fetch('/plugins/varaverk/api/node_chat.php?channel=' + encodeURIComponent(_vvNc.ch) + '&_=' + Date.now()) - .then(r => r.json()) - .then(d => { if (d.ok) { _vvNc.msgs = d.messages || []; _vvNc.lastRead = d.last_read || 0; vvNcRender(); } }) - .catch(() => {}); + .then(r => { + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.json(); + }) + .then(d => { + if (!d.ok) { _vvNcErr('messages', d.error || 'endpoint returned ok:false'); return; } + _vvNcOk(); + _vvNc.msgs = d.messages || []; _vvNc.lastRead = d.last_read || 0; vvNcRender(); + }) + .catch(e => { _vvNcErr('messages', e); }); } function vvNcRender() { @@ -508,7 +554,7 @@ function vvNcRender() { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: new URLSearchParams({ csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '', action: 'read', channel: _vvNc.ch }) - }).catch(() => {}); + }).catch(e => _vvNcErr('read', e)); } } @@ -610,7 +656,7 @@ function vvNcDel(id) { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: new URLSearchParams({ csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '', action: 'delete', channel: _vvNc.ch, id }) - }).then(() => vvNcLoad()).catch(() => {}); + }).then(() => vvNcLoad()).catch(e => _vvNcErr('delete', e)); }