fix(ui): debounce connection-lost banner so transient ws blips don't flash
The reconnect banner showed 'Connection lost'/'Reconnecting' instantly on every socket close, even ones that recover in 100ms-2s (load spikes, Tailscale/relay TCP resets). On a healthy node the drops are brief and self-healing, but each one flashed a jarring banner, reading as constant instability. Debounce the transient banner by 2.5s: only surface after the connection issue persists past the grace window; hide immediately on recovery. Deliberate server lifecycle transitions (restart/shutdown) bypass the debounce and still show at once. A genuine persistent outage keeps isOffline true and surfaces after 2.5s. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7d89b4d8b2
commit
3e3016f2bd
@ -1,9 +1,12 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<!-- Offline Banner -->
|
||||
<!-- Lifecycle / Offline Banner.
|
||||
Server restart/shutdown is deliberate → shown immediately. A plain
|
||||
connection blip is debounced (showConnIssue) so transient sub-grace
|
||||
reconnects don't flash. -->
|
||||
<Transition name="conn-banner">
|
||||
<div
|
||||
v-if="isOffline && !store.isReconnecting && store.isAuthenticated"
|
||||
v-if="(showLifecycle || showConnectionLost)"
|
||||
class="conn-banner-overlay"
|
||||
>
|
||||
<div class="path-option-card px-6 py-3 border-l-4 border-yellow-500 inline-flex items-center gap-2 text-yellow-200 shadow-2xl">
|
||||
@ -17,10 +20,10 @@
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Reconnecting Banner -->
|
||||
<!-- Reconnecting Banner (debounced) -->
|
||||
<Transition name="conn-banner">
|
||||
<div
|
||||
v-if="store.isReconnecting && store.isAuthenticated"
|
||||
v-if="showReconnecting"
|
||||
class="conn-banner-overlay"
|
||||
>
|
||||
<div class="path-option-card px-6 py-3 border-l-4 border-blue-500 inline-flex items-center gap-2 text-blue-200 shadow-2xl">
|
||||
@ -35,7 +38,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, watch, onUnmounted } from 'vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const store = useAppStore()
|
||||
@ -43,6 +46,58 @@ const store = useAppStore()
|
||||
const isOffline = computed(() => store.isOffline)
|
||||
const isRestarting = computed(() => store.isRestarting)
|
||||
const isShuttingDown = computed(() => store.isShuttingDown)
|
||||
|
||||
// A deliberate server lifecycle transition (restart/shutdown) is real and
|
||||
// user-initiated — surface it immediately, no debounce.
|
||||
const isLifecycleTransition = computed(() => isRestarting.value || isShuttingDown.value)
|
||||
const showLifecycle = computed(() => isLifecycleTransition.value && store.isAuthenticated)
|
||||
|
||||
// A plain connection blip (offline or reconnecting, not a lifecycle transition).
|
||||
// The overwhelming majority recover within a second or two (load spikes,
|
||||
// Tailscale/relay TCP resets), so showing the banner instantly makes a healthy
|
||||
// node read as unstable. Debounce: only surface after the issue persists past a
|
||||
// grace window; hide immediately on recovery.
|
||||
const hasConnIssue = computed(
|
||||
() => (store.isReconnecting || isOffline.value) && !isLifecycleTransition.value
|
||||
)
|
||||
|
||||
const SHOW_DELAY_MS = 2500
|
||||
const showConnIssue = ref(false)
|
||||
let pendingTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function clearTimer() {
|
||||
if (pendingTimer) {
|
||||
clearTimeout(pendingTimer)
|
||||
pendingTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
hasConnIssue,
|
||||
(issue) => {
|
||||
clearTimer()
|
||||
if (issue) {
|
||||
pendingTimer = setTimeout(() => {
|
||||
showConnIssue.value = true
|
||||
pendingTimer = null
|
||||
}, SHOW_DELAY_MS)
|
||||
} else {
|
||||
// Recovered before the grace window elapsed — hide at once.
|
||||
showConnIssue.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onUnmounted(clearTimer)
|
||||
|
||||
// Debounced visual states the template renders.
|
||||
const showReconnecting = computed(
|
||||
() => showConnIssue.value && store.isReconnecting && store.isAuthenticated
|
||||
)
|
||||
const showConnectionLost = computed(
|
||||
() => showConnIssue.value && isOffline.value && !store.isReconnecting && store.isAuthenticated
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user