fix(zt-gateway): prevent health dots flashing red on each poll cycle

Cache last-known health state per region so renderGatewayRow uses
correct values instead of defaulting to unreachable. Parallelize
refreshHealth with Promise.all so all gateway pings resolve together
instead of sequentially (3 gateways × -W 3 timeout was ~18s worst case).
This commit is contained in:
2026-07-13 12:18:55 +05:30
parent 1e4a46c4bf
commit 10081b33e2

View File

@@ -59,7 +59,8 @@ function ubusSetup(command) {
return view.extend({ return view.extend({
state: { state: {
selectedRegion: null, selectedRegion: null,
switchMode: 'force' switchMode: 'force',
healthCache: {}
}, },
load() { load() {
@@ -87,11 +88,14 @@ return view.extend({
change: ev => this.onSelectRegion(ev.target.value), change: ev => this.onSelectRegion(ev.target.value),
checked: isSelected || null, checked: isSelected || null,
})), })),
E('td', { class: 'zt-region-cell' }, [ (() => {
this.renderHealthDot(false, null), const cached = this.state.healthCache[gw.region];
' ', const dot = this.renderHealthDot(cached ? cached.reachable : false, cached ? cached.latency_ms : null);
isActive ? E('strong', {}, gw.region) : document.createTextNode(gw.region) return E('td', { class: 'zt-region-cell' }, [
]), dot, ' ',
isActive ? E('strong', {}, gw.region) : document.createTextNode(gw.region)
]);
})(),
E('td', {}, gw.label), E('td', {}, gw.label),
E('td', {}, gw.ip), E('td', {}, gw.ip),
E('td', {}, isActive ? E('em', {}, _('active')) : '') E('td', {}, isActive ? E('em', {}, _('active')) : '')
@@ -250,24 +254,33 @@ return view.extend({
async refreshHealth(data) { async refreshHealth(data) {
const gws = data.gateways || []; const gws = data.gateways || [];
for (let i = 0; i < gws.length; i++) { const checks = gws.map(gw => {
const region = gws[i].region; return ubusHealth(gw.region).then(h => {
try { if (h) {
const h = await ubusHealth(region); this.state.healthCache[gw.region] = {
const row = document.querySelector('.zt-gateway-row[data-region="%s"]'.format(region)); reachable: !!h.reachable,
if (row && h) { latency_ms: h.latency_ms ?? null
const node = row.querySelector('.zt-health'); };
if (node) {
node.classList.toggle('zt-health-up', !!h.reachable);
node.classList.toggle('zt-health-down', !h.reachable);
node.title = h.reachable && h.latency_ms != null
? _('UP (%sms)').format(h.latency_ms)
: (h.reachable ? _('UP') : _('DOWN'));
} }
} return { region: gw.region, h };
} catch (_) { }).catch(() => {
/* health check is best-effort */ this.state.healthCache[gw.region] = { reachable: false, latency_ms: null };
} return { region: gw.region, h: null };
});
});
const results = await Promise.all(checks);
for (const { region, h } of results) {
if (!h) continue;
const row = document.querySelector('.zt-gateway-row[data-region="%s"]'.format(region));
if (!row) continue;
const node = row.querySelector('.zt-health');
if (!node) continue;
node.classList.toggle('zt-health-up', !!h.reachable);
node.classList.toggle('zt-health-down', !h.reachable);
node.title = h.reachable && h.latency_ms != null
? _('UP (%sms)').format(h.latency_ms)
: (h.reachable ? _('UP') : _('DOWN'));
} }
}, },