Compare commits

...
2 Commits
Author SHA1 Message Date
matitos 5af879bc2f Present REST polling as a normal live state
CI/CD / backend (push) Successful in 5m23s
CI/CD / frontend (push) Successful in 4m55s
CI/CD / build (push) Successful in 13s
CI/CD / cleanup (push) Successful in 5s
CI/CD / deploy (push) Successful in 24s
2026-09-20 14:01:38 +00:00
matitos 8bb2836d7e Present REST polling as a normal live state
Some browsers (notably iOS Safari on an auth-gated page) abort the wss://
upgrade even though the server accepts it. Rather than showing a perpetual
"reconnecting", stop retrying after three failures, poll every 3s, and label
the badge "live (polling)" in green. The socket error is kept as a tooltip.
2026-09-20 13:47:42 +00:00
+13 -8
View File
@@ -38,12 +38,15 @@ const busy = ref(false)
const connected = ref(false) const connected = ref(false)
const socketError = ref('') const socketError = ref('')
const socketFailures = ref(0) const socketFailures = ref(0)
// After a few failed socket attempts (some mobile browsers/DPIs refuse wss://), // Some mobile browsers (notably iOS Safari with an auth-gated page) abort the
// stop advertising "reconnecting" and rely on the 5s REST poll instead. // wss:// upgrade even when the server accepts it. After a few failures we stop
// retrying, rely on the REST poll, and present it as a normal live state rather
// than a degraded "reconnecting".
const usePolling = computed(() => socketFailures.value >= 3) const usePolling = computed(() => socketFailures.value >= 3)
let socket let socket
let pollTimer let pollTimer
let statusTimer
async function refreshAll() { async function refreshAll() {
const [p, o, e, q] = await Promise.all([ const [p, o, e, q] = await Promise.all([
@@ -148,12 +151,15 @@ onMounted(async () => {
else socketError.value = `socket closed (code ${code})` else socketError.value = `socket closed (code ${code})`
}, },
}) })
pollTimer = setInterval(refreshAll, 5000) // Poll fast enough that the UI feels live even when it is the only channel.
pollTimer = setInterval(refreshAll, 3000)
statusTimer = setInterval(() => api.getStatus().then((s) => (status.value = s)), 3000)
}) })
onUnmounted(() => { onUnmounted(() => {
socket?.close() socket?.close()
clearInterval(pollTimer) clearInterval(pollTimer)
clearInterval(statusTimer)
}) })
</script> </script>
@@ -168,15 +174,14 @@ onUnmounted(() => {
> >
{{ status.mode }} {{ status.mode }}
</span> </span>
<span class="flex items-center gap-1.5 text-xs text-slate-400"> <span class="flex items-center gap-1.5 text-xs text-slate-400" :title="socketError || ''">
<span <span
class="h-2 w-2 rounded-full" class="h-2 w-2 rounded-full"
:class="connected ? 'bg-emerald-400' : usePolling ? 'bg-sky-400' : 'bg-slate-500'" :class="connected || usePolling ? 'bg-emerald-400' : 'bg-slate-500'"
></span> ></span>
<template v-if="connected">live</template> <template v-if="connected">live</template>
<template v-else-if="usePolling">polling (5s)</template> <template v-else-if="usePolling">live (polling)</template>
<template v-else>reconnecting</template> <template v-else>connecting…</template>
<span v-if="!connected && socketError" class="text-amber-300">· {{ socketError }}</span>
</span> </span>
</div> </div>
<div class="text-xs text-slate-400"> <div class="text-xs text-slate-400">