fix: harden ElectrumX status — cached backend, stable frontend

Backend: cache status in RwLock, refresh every 15s via background task.
Eliminates per-request TCP race to ElectrumX that caused volatile errors.
Fix error classification so "Failed to read" is transient, not hard error.

Frontend: keep last-known-good data across failed polls, persist Tor
onion once discovered, adaptive polling (5s active / 30s synced).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-09 10:32:55 +02:00
co-authored by Claude Opus 4.6
parent 07dff3e4ca
commit d0b9f168f4
3 changed files with 186 additions and 84 deletions
+91 -57
View File
@@ -268,6 +268,11 @@
<script>
var currentTab = 'local';
var torOnion = null;
var lastGoodData = null;
var consecutiveFailures = 0;
var pollTimer = null;
var POLL_ACTIVE = 5000; // 5s while syncing/indexing/starting
var POLL_SYNCED = 30000; // 30s when synced
function renderQR(containerId, text) {
var container = document.getElementById(containerId);
@@ -353,84 +358,113 @@
}
}
function schedulePoll(intervalMs) {
if (pollTimer) clearTimeout(pollTimer);
pollTimer = setTimeout(function() { updateStatus(); }, intervalMs);
}
function applyData(data) {
// Persist tor onion once discovered — never flicker back to "not configured"
if (data.tor_onion) {
applyTorOnion(data.tor_onion);
}
var indexedH = data.indexed_height || 0;
var networkH = data.network_height || 0;
var pct = data.progress_pct || 0;
document.getElementById('indexedHeight').textContent = indexedH > 0 ? indexedH.toLocaleString() : (data.status === 'indexing' ? 'Building...' : '-');
document.getElementById('networkHeight').textContent = networkH > 0 ? networkH.toLocaleString() : '-';
document.getElementById('indexSize').textContent = data.index_size || '-';
document.getElementById('progressPct').textContent = pct > 0 ? pct.toFixed(1) + '%' : '-';
document.getElementById('currentBlock').textContent = indexedH > 0 ? 'Block ' + indexedH.toLocaleString() : (data.index_size ? 'Index: ' + data.index_size : 'Block 0');
document.getElementById('syncPercentage').textContent = pct > 0 ? pct.toFixed(1) + '%' : '0%';
document.getElementById('syncProgressBar').style.width = Math.max(pct, 0.5) + '%';
var statusTextEl = document.getElementById('syncStatusText');
var statusDot = document.getElementById('statusDot');
var syncIcon = document.getElementById('syncIcon');
if (data.status === 'starting' || data.status === 'waiting') {
statusTextEl.textContent = data.error || 'Starting up...';
statusTextEl.style.color = '#fbbf24';
statusDot.className = 'status-dot bg-yellow animate-pulse';
document.getElementById('statusText').textContent = 'Starting';
syncIcon.classList.add('animate-spin-slow');
document.getElementById('connSubtitle').textContent = 'Connections will be available once ElectrumX has completed syncing.';
} else if (data.status === 'indexing') {
statusTextEl.textContent = data.error || 'Building index...';
statusTextEl.style.color = '#fbbf24';
statusDot.className = 'status-dot bg-amber animate-pulse';
document.getElementById('statusText').textContent = 'Indexing';
syncIcon.classList.add('animate-spin-slow');
document.getElementById('connSubtitle').textContent = 'Connections will be available once ElectrumX has completed syncing.';
} else if (data.status === 'error') {
statusTextEl.textContent = data.error || 'Unknown error';
statusTextEl.style.color = '#f87171';
statusDot.className = 'status-dot bg-red';
document.getElementById('statusText').textContent = 'Error';
document.getElementById('connSubtitle').textContent = 'Connections will be available once ElectrumX has completed syncing.';
} else if (data.status === 'synced') {
statusTextEl.textContent = 'Fully synchronized with the network';
statusTextEl.style.color = '#4ade80';
statusDot.className = 'status-dot bg-green';
document.getElementById('statusText').textContent = 'Synced';
syncIcon.classList.remove('animate-spin-slow');
syncIcon.style.color = '#4ade80';
document.getElementById('connSubtitle').textContent = 'Use the following details to connect your wallet or application to ElectrumX.';
} else {
var remaining = networkH - indexedH;
statusTextEl.textContent = 'Syncing... ' + remaining.toLocaleString() + ' blocks remaining';
statusTextEl.style.color = '#fb923c';
statusDot.className = 'status-dot bg-yellow';
document.getElementById('statusText').textContent = 'Syncing';
syncIcon.classList.add('animate-spin-slow');
document.getElementById('connSubtitle').textContent = 'Connections will be available once ElectrumX has completed syncing.';
}
}
async function updateStatus() {
try {
var resp = await fetch('/electrs-status', { cache: 'no-store' });
if (!resp.ok) {
throw new Error('Backend unavailable (HTTP ' + resp.status + ')');
throw new Error('backend_unavailable');
}
var text = await resp.text();
if (text.trim().charAt(0) !== '{') {
throw new Error('Waiting for Archipelago backend...');
throw new Error('backend_starting');
}
var data = JSON.parse(text);
// Extract Tor onion from status response
if (data.tor_onion && !torOnion) {
applyTorOnion(data.tor_onion);
}
// Success — reset failure counter, save as last known good
consecutiveFailures = 0;
lastGoodData = data;
applyData(data);
var indexedH = data.indexed_height || 0;
var networkH = data.network_height || 0;
var pct = data.progress_pct || 0;
document.getElementById('indexedHeight').textContent = indexedH > 0 ? indexedH.toLocaleString() : (data.status === 'indexing' ? 'Building...' : '-');
document.getElementById('networkHeight').textContent = networkH > 0 ? networkH.toLocaleString() : '-';
document.getElementById('indexSize').textContent = data.index_size || '-';
document.getElementById('progressPct').textContent = pct > 0 ? pct.toFixed(1) + '%' : '-';
document.getElementById('currentBlock').textContent = indexedH > 0 ? 'Block ' + indexedH.toLocaleString() : (data.index_size ? 'Index: ' + data.index_size : 'Block 0');
document.getElementById('syncPercentage').textContent = pct > 0 ? pct.toFixed(1) + '%' : '0%';
document.getElementById('syncProgressBar').style.width = Math.max(pct, 0.5) + '%';
var statusTextEl = document.getElementById('syncStatusText');
var statusDot = document.getElementById('statusDot');
var syncIcon = document.getElementById('syncIcon');
if (data.status === 'indexing') {
statusTextEl.textContent = data.error || 'Building index...';
statusTextEl.style.color = '#fbbf24';
statusDot.className = 'status-dot bg-amber animate-pulse';
document.getElementById('statusText').textContent = 'Indexing';
syncIcon.classList.add('animate-spin-slow');
document.getElementById('connSubtitle').textContent = 'Connections will be available once ElectrumX has completed syncing.';
} else if (data.status === 'error') {
statusTextEl.textContent = data.error || 'Unknown error';
statusTextEl.style.color = '#f87171';
statusDot.className = 'status-dot bg-red';
document.getElementById('statusText').textContent = 'Error';
document.getElementById('connSubtitle').textContent = 'Connections will be available once ElectrumX has completed syncing.';
} else if (data.status === 'synced') {
statusTextEl.textContent = 'Fully synchronized with the network';
statusTextEl.style.color = '#4ade80';
statusDot.className = 'status-dot bg-green';
document.getElementById('statusText').textContent = 'Synced';
syncIcon.classList.remove('animate-spin-slow');
syncIcon.style.color = '#4ade80';
document.getElementById('connSubtitle').textContent = 'Use the following details to connect your wallet or application to ElectrumX.';
} else {
var remaining = networkH - indexedH;
statusTextEl.textContent = 'Syncing... ' + remaining.toLocaleString() + ' blocks remaining';
statusTextEl.style.color = '#fb923c';
statusDot.className = 'status-dot bg-yellow';
document.getElementById('statusText').textContent = 'Syncing';
syncIcon.classList.add('animate-spin-slow');
document.getElementById('connSubtitle').textContent = 'Connections will be available once ElectrumX has completed syncing.';
}
// Adaptive polling — slower when synced
schedulePoll(data.status === 'synced' ? POLL_SYNCED : POLL_ACTIVE);
} catch (e) {
var msg = e.message || 'Unknown error';
if (msg.indexOf('HTTP 5') !== -1 || msg.indexOf('Failed to fetch') !== -1 || msg.indexOf('NetworkError') !== -1) {
msg = 'Waiting for Archipelago backend...';
consecutiveFailures++;
if (lastGoodData && consecutiveFailures <= 3) {
// Keep showing last known good data — don't flicker the UI
// Just poll again sooner to recover
schedulePoll(POLL_ACTIVE);
return;
}
document.getElementById('syncStatusText').textContent = msg;
// No prior good data, or too many failures — show connecting state
document.getElementById('syncStatusText').textContent = 'Connecting to Archipelago...';
document.getElementById('syncStatusText').style.color = '#fbbf24';
document.getElementById('statusDot').className = 'status-dot bg-yellow animate-pulse';
document.getElementById('statusText').textContent = 'Connecting';
schedulePoll(POLL_ACTIVE);
}
}
updateStatus();
updateConnectionInfo();
setInterval(updateStatus, 5000);
</script>
</body>
</html>