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.
This commit is contained in:
Gmer4Lfe
2026-08-21 09:00:19 -04:00
parent 8041da5424
commit e1cdf03d40
+55 -9
View File
@@ -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));
}