Explorar o código

fix: Much more helpful reconnection banner

jamesread hai 3 meses
pai
achega
a5c102dbf1

+ 11 - 4
frontend/js/websocket.js

@@ -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()

+ 2 - 16
frontend/resources/vue/App.vue

@@ -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([])
 

+ 68 - 0
frontend/resources/vue/components/ConnectionBanner.vue

@@ -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>

+ 3 - 1
frontend/resources/vue/stores/connectionState.js

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

+ 10 - 0
lang/combined_output.json

@@ -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": "关闭",

+ 2 - 0
lang/de-DE.yaml

@@ -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

+ 2 - 0
lang/en.yaml

@@ -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.

+ 2 - 0
lang/es-ES.yaml

@@ -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

+ 2 - 0
lang/it-IT.yaml

@@ -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

+ 2 - 0
lang/zh-Hans-CN.yaml

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