fix: LiveStats: show hard error when Ponder is unreachable (not silent skeletons) (#201)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-02-24 21:33:50 +00:00
parent 4125efc8f0
commit 3426fbf80b

View file

@ -1,5 +1,5 @@
<template> <template>
<div v-if="!error && stats" class="live-stats"> <div v-if="stats" class="live-stats">
<div class="live-header"> <div class="live-header">
<span class="live-dot" :class="{ 'live-dot-error': error }"></span> <span class="live-dot" :class="{ 'live-dot-error': error }"></span>
<span class="live-text">Live</span> <span class="live-text">Live</span>
@ -46,7 +46,7 @@
</div> </div>
<div class="freshness">{{ error ? 'Connection lost' : `Updated ${secondsSinceUpdate}s ago` }}</div> <div class="freshness">{{ error ? 'Connection lost' : `Updated ${secondsSinceUpdate}s ago` }}</div>
</div> </div>
<div v-else-if="!error && !stats" class="live-stats"> <div v-else-if="!error" class="live-stats">
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-item skeleton" v-for="i in 6" :key="i"> <div class="stat-item skeleton" v-for="i in 6" :key="i">
<div class="stat-label skeleton-text"></div> <div class="stat-label skeleton-text"></div>
@ -54,6 +54,9 @@
</div> </div>
</div> </div>
</div> </div>
<div v-else class="live-stats live-stats-error" data-testid="livestats-error">
<p class="error-message">Protocol data unavailable</p>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -88,6 +91,8 @@ const changedStats = ref<Set<string>>(new Set());
const secondsSinceUpdate = ref(0); const secondsSinceUpdate = ref(0);
let freshnessTicker: number | null = null; let freshnessTicker: number | null = null;
let changeTimeout: number | null = null; let changeTimeout: number | null = null;
let loadingTimer: number | null = null;
const LOADING_TIMEOUT_MS = 10_000; // escalate to error after 10s with no data
// Helper: safely convert a Wei BigInt string to ETH float // Helper: safely convert a Wei BigInt string to ETH float
function weiToEth(wei: string | null | undefined): number { function weiToEth(wei: string | null | undefined): number {
@ -417,6 +422,7 @@ async function fetchStats() {
stats.value = s; stats.value = s;
secondsSinceUpdate.value = 0; secondsSinceUpdate.value = 0;
error.value = false; error.value = false;
if (loadingTimer) { clearTimeout(loadingTimer); loadingTimer = null; }
} else { } else {
throw new Error('No stats data'); throw new Error('No stats data');
} }
@ -428,6 +434,10 @@ async function fetchStats() {
} }
onMounted(async () => { onMounted(async () => {
// Escalate to error state if no data arrives within the timeout window
loadingTimer = window.setTimeout(() => {
if (!stats.value) error.value = true;
}, LOADING_TIMEOUT_MS);
await fetchStats(); await fetchStats();
fetchEthPrice(); fetchEthPrice();
refreshInterval = window.setInterval(fetchStats, 30000); // Refresh every 30 seconds refreshInterval = window.setInterval(fetchStats, 30000); // Refresh every 30 seconds
@ -440,6 +450,7 @@ onUnmounted(() => {
if (ethPriceInterval) clearInterval(ethPriceInterval); if (ethPriceInterval) clearInterval(ethPriceInterval);
if (freshnessTicker) clearInterval(freshnessTicker); if (freshnessTicker) clearInterval(freshnessTicker);
if (changeTimeout) clearTimeout(changeTimeout); if (changeTimeout) clearTimeout(changeTimeout);
if (loadingTimer) clearTimeout(loadingTimer);
}); });
</script> </script>
@ -610,4 +621,15 @@ onUnmounted(() => {
background-position: 200% 0 background-position: 200% 0
100% 100%
background-position: -200% 0 background-position: -200% 0
.live-stats-error
display: flex
align-items: center
justify-content: center
.error-message
font-size: 14px
color: #f87171
text-align: center
letter-spacing: 0.3px
</style> </style>