fix: Much more helpful reconnection banner

This commit is contained in:
jamesread 2026-03-09 00:27:53 +00:00
parent 2f77000de4
commit a5c102dbf1
10 changed files with 104 additions and 21 deletions

View File

@ -2,7 +2,7 @@ import { buttonResults } from '../resources/vue/stores/buttonResults.js'
import { rateLimits } from '../resources/vue/stores/rateLimits.js'
import { connectionState } from '../resources/vue/stores/connectionState.js'
const RECONNECT_DELAY_MS = 3000
const RECONNECT_DELAY_MS = 10000
export function initWebsocket () {
window.addEventListener('EventOutputChunk', onOutputChunk)
@ -21,12 +21,17 @@ async function reconnectWebsocket () {
connectionState.reconnecting = true
connectionState.connected = false
connectionState.disconnectedAt = Date.now()
connectionState.nextReconnectAt = null
try {
window.websocketAvailable = true
for await (const e of window.client.eventStream()) {
connectionState.connected = true
connectionState.reconnecting = false
const stream = window.client.eventStream()
connectionState.connected = true
connectionState.reconnecting = false
connectionState.disconnectedAt = null
connectionState.nextReconnectAt = null
for await (const e of stream) {
handleEvent(e)
}
} catch (err) {
@ -35,6 +40,8 @@ async function reconnectWebsocket () {
window.websocketAvailable = false
connectionState.connected = false
connectionState.disconnectedAt = connectionState.disconnectedAt ?? Date.now()
connectionState.nextReconnectAt = Date.now() + RECONNECT_DELAY_MS
console.log('Reconnecting websocket in ' + RECONNECT_DELAY_MS + 'ms...')
setTimeout(() => {
reconnectWebsocket()

View File

@ -7,6 +7,7 @@
</template>
<template #user-info>
<ConnectionBanner />
<div class="flex-row user-info" style="gap: .5em;">
<span id="link-login" v-if="!isLoggedIn && showLoginLink"><router-link to="/login">{{ t('login-button') }}</router-link></span>
<router-link v-else to="/user" class="user-link" v-if="isLoggedIn">
@ -49,8 +50,6 @@
<span v-if="availableThemes.length > 1">
<a href="#" @click.prevent="openThemeDialog">{{ currentThemeName }}</a>
</span>
<span :title="connectionStatusTitle">{{ connectionStatusLabel }}</span>
</p>
<p v-if="showVersionNumber">
<a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a>
@ -100,6 +99,7 @@ import { useRouter } from 'vue-router';
import Sidebar from 'picocrank/vue/components/Sidebar.vue';
import Navigation from 'picocrank/vue/components/Navigation.vue';
import Header from 'picocrank/vue/components/Header.vue';
import ConnectionBanner from './components/ConnectionBanner.vue';
import { HugeiconsIcon } from '@hugeicons/vue'
import { Menu01Icon } from '@hugeicons/core-free-icons'
import { UserCircle02Icon } from '@hugeicons/core-free-icons'
@ -107,8 +107,6 @@ import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
import logoUrl from '../../OliveTinLogo.png';
import { useI18n } from 'vue-i18n';
import combinedTranslations from '../../../lang/combined_output.json';
import { connectionState } from './stores/connectionState.js';
const { t, locale } = useI18n();
const router = useRouter();
@ -130,18 +128,6 @@ const showVersionNumber = ref(true)
const showLoginLink = ref(true)
const sectionNavigationStyle = ref('sidebar')
const connectionStatusLabel = computed(() => {
if (connectionState.connected) {
return t('connected')
}
if (connectionState.reconnecting) {
return t('reconnecting')
}
return t('disconnected')
})
const connectionStatusTitle = computed(() => connectionStatusLabel.value)
const languageDialog = ref(null)
const browserLanguages = ref([])

View File

@ -0,0 +1,68 @@
<template>
<span id="connection-banner" v-if="!connectionState.connected" class="inline-notification critical user-info-connection" role="status">{{ bannerText }}</span>
</template>
<script setup>
import { ref, computed, watch, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { connectionState } from '../stores/connectionState.js'
const { t } = useI18n()
function formatShortRelative(ms) {
if (ms < 0) return '0s'
const secs = Math.floor(ms / 1000)
const mins = Math.floor(secs / 60)
const hours = Math.floor(mins / 60)
if (hours > 0) return `${hours}h`
if (mins > 0) return `${mins}m`
return `${secs}s`
}
function formatShortTime(ts) {
if (ts == null) return '--:--'
return new Date(ts).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
}
const now = ref(Date.now())
let ticker = null
watch(() => connectionState.connected, (connected) => {
if (ticker) {
clearInterval(ticker)
ticker = null
}
if (!connected) {
now.value = Date.now()
ticker = setInterval(() => { now.value = Date.now() }, 1000)
}
}, { immediate: true })
onUnmounted(() => {
if (ticker) {
clearInterval(ticker)
ticker = null
}
})
const bannerText = computed(() => {
const at = connectionState.disconnectedAt
const next = connectionState.nextReconnectAt
const n = now.value
const disconnectedSince = formatShortTime(at)
if (next != null && next > n) {
const reconnectIn = formatShortRelative(next - n)
return t('disconnected-banner', { disconnectedSince, reconnectIn })
}
return t('disconnected-banner-reconnecting', { disconnectedSince })
})
</script>
<style scoped>
#connection-banner.user-info-connection {
font-weight: 500;
}
.inline-notification {
border: 0;
margin: 0;
}
</style>

View File

@ -2,5 +2,7 @@ import { reactive } from 'vue'
export const connectionState = reactive({
connected: false,
reconnecting: false
reconnecting: false,
disconnectedAt: null,
nextReconnectAt: null
})

View File

@ -21,6 +21,8 @@
"diagnostics.useragent-data-error": "Fehler beim Abrufen von userAgentData",
"diagnostics.where-to-find-help": "Wo Sie Hilfe finden",
"disconnected": "Getrennt",
"disconnected-banner": "Events-Websocket getrennt seit {disconnectedSince}. Erneuter Verbindungsversuch in {reconnectIn}.",
"disconnected-banner-reconnecting": "Events-Websocket getrennt seit {disconnectedSince}. Verbindungsversuch…",
"docs": "Dokumentation",
"language-dialog.browser-languages": "Browser-Sprachen",
"language-dialog.close": "Schließen",
@ -76,6 +78,8 @@
"diagnostics.useragent-data-error": "Error retrieving userAgentData",
"diagnostics.where-to-find-help": "Where to find help",
"disconnected": "Disconnected",
"disconnected-banner": "Events websocket disconnected since {disconnectedSince}. Trying reconnect in {reconnectIn}.",
"disconnected-banner-reconnecting": "Events websocket disconnected since {disconnectedSince}. Trying reconnect…",
"docs": "Documentation",
"language-dialog.browser-languages": "Browser languages",
"language-dialog.close": "Close",
@ -131,6 +135,8 @@
"diagnostics.useragent-data-error": "Error al recuperar userAgentData",
"diagnostics.where-to-find-help": "Dónde encontrar ayuda",
"disconnected": "Desconectado",
"disconnected-banner": "Websocket de eventos desconectado desde {disconnectedSince}. Reintentando conexión en {reconnectIn}.",
"disconnected-banner-reconnecting": "Websocket de eventos desconectado desde {disconnectedSince}. Reintentando conexión…",
"docs": "Documentación",
"language-dialog.browser-languages": "Idiomas del navegador",
"language-dialog.close": "Cerrar",
@ -186,6 +192,8 @@
"diagnostics.useragent-data-error": "Errore nel recupero di userAgentData",
"diagnostics.where-to-find-help": "Dove trovare aiuto",
"disconnected": "Disconnesso",
"disconnected-banner": "Websocket eventi disconnesso dalle {disconnectedSince}. Nuovo tentativo tra {reconnectIn}.",
"disconnected-banner-reconnecting": "Websocket eventi disconnesso dalle {disconnectedSince}. Tentativo di connessione…",
"docs": "Documentazione",
"language-dialog.browser-languages": "Lingue del browser",
"language-dialog.close": "Chiudi",
@ -241,6 +249,8 @@
"diagnostics.useragent-data-error": "检索 userAgentData 时出错",
"diagnostics.where-to-find-help": "在哪里找到帮助",
"disconnected": "已断开连接",
"disconnected-banner": "事件 WebSocket 自 {disconnectedSince} 已断开。{reconnectIn} 后尝试重连。",
"disconnected-banner-reconnecting": "事件 WebSocket 自 {disconnectedSince} 已断开。正在尝试重连…",
"docs": "文档",
"language-dialog.browser-languages": "浏览器语言",
"language-dialog.close": "关闭",

View File

@ -8,6 +8,8 @@ translations:
connected: Verbunden
disconnected: Getrennt
reconnecting: Verbinde erneut…
disconnected-banner: "Events-Websocket getrennt seit {disconnectedSince}. Erneuter Verbindungsversuch in {reconnectIn}."
disconnected-banner-reconnecting: "Events-Websocket getrennt seit {disconnectedSince}. Verbindungsversuch…"
login-button: Login
raise-issue: Ein Problem melden auf GitHub
docs: Dokumentation

View File

@ -10,6 +10,8 @@ translations:
connected: Connected
disconnected: Disconnected
reconnecting: Reconnecting…
disconnected-banner: "Events websocket disconnected since {disconnectedSince}. Trying reconnect in {reconnectIn}."
disconnected-banner-reconnecting: "Events websocket disconnected since {disconnectedSince}. Trying reconnect…"
login-button: Login
logs.title: Logs
logs.page-description: This is a list of logs from actions that have been executed. You can filter the list by action title.

View File

@ -8,6 +8,8 @@ translations:
connected: Conectado
disconnected: Desconectado
reconnecting: Reconectando…
disconnected-banner: "Websocket de eventos desconectado desde {disconnectedSince}. Reintentando conexión en {reconnectIn}."
disconnected-banner-reconnecting: "Websocket de eventos desconectado desde {disconnectedSince}. Reintentando conexión…"
login-button: Iniciar sesión
raise-issue: Reportar un problema en GitHub
docs: Documentación

View File

@ -9,6 +9,8 @@ translations:
connected: Connesso
disconnected: Disconnesso
reconnecting: Riconnessione…
disconnected-banner: "Websocket eventi disconnesso dalle {disconnectedSince}. Nuovo tentativo tra {reconnectIn}."
disconnected-banner-reconnecting: "Websocket eventi disconnesso dalle {disconnectedSince}. Tentativo di connessione…"
login-button: Login
raise-issue: Segnala un problema su GitHub
logs.title: Registri

View File

@ -8,6 +8,8 @@ translations:
connected: 已连接
disconnected: 已断开连接
reconnecting: 正在重新连接…
disconnected-banner: "事件 WebSocket 自 {disconnectedSince} 已断开。{reconnectIn} 后尝试重连。"
disconnected-banner-reconnecting: "事件 WebSocket 自 {disconnectedSince} 已断开。正在尝试重连…"
login-button: 登录
raise-issue: 在 GitHub 上报告问题
docs: 文档