diff --git a/neode-ui/src/views/Monitoring.vue b/neode-ui/src/views/Monitoring.vue index 9c12b936..62094e30 100644 --- a/neode-ui/src/views/Monitoring.vue +++ b/neode-ui/src/views/Monitoring.vue @@ -218,6 +218,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' import { rpcClient } from '@/api/rpc-client' +import { useCachedResource } from '@/composables/useCachedResource' import { useHomeStatusStore } from '@/stores/homeStatus' import BackButton from '@/components/BackButton.vue' import LineChart from '@/components/LineChart.vue' @@ -291,11 +292,51 @@ const backTarget = computed(() => (cameFromHome.value ? '/dashboard' : '/dashboa const backLabel = computed(() => (cameFromHome.value ? t('common.back') : 'Web5')) const homeStatus = useHomeStatusStore() -const current = ref(null) -const history = ref([]) -const containers = ref([]) -const alerts = ref([]) -const alertRules = ref([]) +// Cached: revisits paint the last snapshot/chart/alerts instantly and the 5s +// poll revalidates behind them; errors keep the last-known values. +const currentRes = useCachedResource({ + key: 'monitoring.current', + fetcher: async (signal) => { + const data = await rpcClient.call({ + method: 'monitoring.current', signal, dedup: true, maxRetries: 1, + }) + if (!data || !('system' in data)) throw new Error('metrics not ready') + return data + }, +}) +const historyRes = useCachedResource({ + key: 'monitoring.history.minute60', + fetcher: async (signal) => { + const data = await rpcClient.call({ + method: 'monitoring.history', params: { resolution: 'minute', count: 60 }, + signal, dedup: true, maxRetries: 1, + }) + return data?.data ?? [] + }, +}) +const alertsRes = useCachedResource({ + key: 'monitoring.alerts', + fetcher: async (signal) => { + const data = await rpcClient.call<{ alerts: FiredAlert[] }>({ + method: 'monitoring.alerts', params: { count: 50 }, signal, dedup: true, maxRetries: 1, + }) + return (data?.alerts ?? []).reverse() + }, +}) +const alertRulesRes = useCachedResource({ + key: 'monitoring.alert-rules', + fetcher: async (signal) => { + const data = await rpcClient.call<{ rules: AlertRule[] }>({ + method: 'monitoring.alert-rules', signal, dedup: true, maxRetries: 1, + }) + return data?.rules ?? [] + }, +}) +const current = computed(() => currentRes.data.value) +const history = computed(() => historyRes.data.value ?? []) +const containers = computed(() => currentRes.data.value?.containers ?? []) +const alerts = computed(() => alertsRes.data.value ?? []) +const alertRules = computed(() => alertRulesRes.data.value ?? []) const showAlertConfig = ref(false) const chartWidth = ref(380) let pollTimer: ReturnType | null = null @@ -464,66 +505,10 @@ async function exportMetrics(format: 'csv' | 'json') { } } -async function fetchCurrent() { - try { - await homeStatus.refreshSystemStats() - const data = await rpcClient.call({ - method: 'monitoring.current', - }) - if (data && 'system' in data) { - current.value = data - containers.value = data.containers ?? [] - } - } catch { - // Silently retry on next poll - } -} - -async function fetchHistory() { - try { - const data = await rpcClient.call({ - method: 'monitoring.history', - params: { resolution: 'minute', count: 60 }, - }) - if (data?.data) { - history.value = data.data - } - } catch { - // Silently retry on next poll - } -} - -async function fetchAlerts() { - try { - const data = await rpcClient.call<{ alerts: FiredAlert[] }>({ - method: 'monitoring.alerts', - params: { count: 50 }, - }) - if (data?.alerts) { - alerts.value = data.alerts.reverse() - } - } catch { - // Silently retry on next poll - } -} - -async function fetchAlertRules() { - try { - const data = await rpcClient.call<{ rules: AlertRule[] }>({ - method: 'monitoring.alert-rules', - }) - if (data?.rules) { - alertRules.value = data.rules - } - } catch { - // Non-critical - } -} - async function toggleAlertRule(kind: string, enabled: boolean) { try { await rpcClient.call({ method: 'monitoring.configure-alert', params: { kind, enabled } }) - await fetchAlertRules() + await alertRulesRes.refresh() } catch { // Non-critical } @@ -534,7 +519,7 @@ async function updateThreshold(kind: string, value: string) { if (isNaN(threshold) || threshold <= 0) return try { await rpcClient.call({ method: 'monitoring.configure-alert', params: { kind, threshold } }) - await fetchAlertRules() + await alertRulesRes.refresh() } catch { // Non-critical } @@ -543,7 +528,7 @@ async function updateThreshold(kind: string, value: string) { async function acknowledgeAlert(id: string) { try { await rpcClient.call({ method: 'monitoring.acknowledge-alert', params: { id } }) - await fetchAlerts() + await alertsRes.refresh() } catch { // Non-critical } @@ -556,18 +541,18 @@ function updateChartWidth() { } } -onMounted(async () => { +onMounted(() => { updateChartWidth() window.addEventListener('resize', updateChartWidth) - await Promise.all([fetchCurrent(), fetchHistory(), fetchAlerts(), fetchAlertRules()]) - - pollTimer = setInterval(async () => { - try { - await Promise.all([fetchCurrent(), fetchHistory(), fetchAlerts()]) - } catch { - // Background poll — ignore transient errors - } + // The cached resources fetch themselves on first use; the poll keeps the + // live view fresh (refreshes dedup in the store). + void homeStatus.refreshSystemStats() + pollTimer = setInterval(() => { + void homeStatus.refreshSystemStats() + void currentRes.refresh() + void historyRes.refresh() + void alertsRes.refresh() }, 5000) })