fix: WebSocket reconnect state refresh, listener leak fixes, pin container images

- F4: Fetch fresh server state after WebSocket reconnect
- F5: Guard message polling timer with auth check, stop on logout
- F6: Remove NIP-07 listener in appLauncher close()
- F7: Initialize audio player once to prevent listener stacking
- S3: Pin all container images to specific versions, create image-versions.sh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-21 01:32:28 +00:00
co-authored by Claude Opus 4.6
parent 4d17c60da7
commit 3b7d541224
13 changed files with 233 additions and 110 deletions
+33 -27
View File
@@ -7,46 +7,52 @@ const playing = ref(false)
const currentTime = ref(0)
const duration = ref(0)
const error = ref<string | null>(null)
let initialized = false
/** Create the Audio element and attach listeners once */
function init() {
if (initialized) return
initialized = true
audio.value = new Audio()
audio.value.addEventListener('timeupdate', () => {
currentTime.value = audio.value?.currentTime ?? 0
})
audio.value.addEventListener('loadedmetadata', () => {
duration.value = audio.value?.duration ?? 0
error.value = null
})
audio.value.addEventListener('ended', () => {
playing.value = false
})
audio.value.addEventListener('pause', () => {
playing.value = false
})
audio.value.addEventListener('play', () => {
playing.value = true
error.value = null
})
audio.value.addEventListener('error', () => {
playing.value = false
error.value = 'Could not play audio. File Browser may not be running.'
})
}
function play(src: string, name: string) {
if (!audio.value) {
audio.value = new Audio()
audio.value.addEventListener('timeupdate', () => {
currentTime.value = audio.value?.currentTime ?? 0
})
audio.value.addEventListener('loadedmetadata', () => {
duration.value = audio.value?.duration ?? 0
error.value = null
})
audio.value.addEventListener('ended', () => {
playing.value = false
})
audio.value.addEventListener('pause', () => {
playing.value = false
})
audio.value.addEventListener('play', () => {
playing.value = true
error.value = null
})
audio.value.addEventListener('error', () => {
playing.value = false
error.value = 'Could not play audio. File Browser may not be running.'
})
}
init()
error.value = null
if (currentSrc.value === src && playing.value) {
audio.value.pause()
audio.value!.pause()
return
}
if (currentSrc.value !== src) {
audio.value.src = src
audio.value!.src = src
currentSrc.value = src
currentName.value = name
}
audio.value.play()
audio.value!.play()
}
function pause() {
+12 -1
View File
@@ -54,10 +54,21 @@ export function useMessageToast() {
}
}
function isAuthenticated(): boolean {
return localStorage.getItem('neode-auth') === 'true'
}
function startPolling() {
if (pollTimer) return
if (!isAuthenticated()) return
loadReceivedMessages()
pollTimer = setInterval(loadReceivedMessages, MESSAGE_POLL_INTERVAL)
pollTimer = setInterval(() => {
if (!isAuthenticated()) {
stopPolling()
return
}
loadReceivedMessages()
}, MESSAGE_POLL_INTERVAL)
}
function stopPolling() {
+12 -1
View File
@@ -149,7 +149,18 @@ export const useAppStore = defineStore('app', () => {
await wsClient.connect()
if (import.meta.env.DEV) console.log('[Store] WebSocket connected')
// Fetch fresh state after reconnect to avoid stale patch application
try {
const freshState = await rpcClient.call<{ data: DataModel }>({ method: 'server.get-state' })
if (freshState?.data) {
data.value = freshState.data
}
} catch {
// Non-fatal: WebSocket patches will still work
if (import.meta.env.DEV) console.warn('[Store] Failed to fetch fresh state after reconnect')
}
// Connection state will be updated via the callback
if (wsClient.isConnected()) {
isConnected.value = true
+3
View File
@@ -162,6 +162,9 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
isOpen.value = false
url.value = ''
title.value = ''
// Explicitly remove NIP-07 listener as safety net — if user navigates away
// without close() triggering the isOpen watcher, the listener would leak
window.removeEventListener('message', handleNostrRequest)
if (toRestore && typeof toRestore.focus === 'function') {
requestAnimationFrame(() => {
toRestore.focus()