fix: WebSocket race conditions, Vue error handler, remove sudo podman, add container health checks

- F1: Guard connectWebSocket against concurrent calls with isWsConnecting flag
- F2: Serialize mesh send operations with sendQueue to prevent fetchMessages races
- F3: Add global Vue error handler with toast notification
- S1: Replace sudo podman with podman across all scripts (rootless Podman)
- S2: Add health-cmd to all 40 container run commands in first-boot-containers.sh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-21 01:11:05 +00:00
parent b57ca4f171
commit 1d98de24d0
10 changed files with 301 additions and 110 deletions

View File

@@ -4,6 +4,7 @@ import './style.css'
import App from './App.vue'
import router from './router'
import i18n from './i18n'
import { useToast } from '@/composables/useToast'
const app = createApp(App)
const pinia = createPinia()
@@ -12,4 +13,10 @@ app.use(pinia)
app.use(router)
app.use(i18n)
app.config.errorHandler = (err, _instance, info) => {
console.error('[Vue Error]', err, info)
const { error } = useToast()
error('Something went wrong. Please refresh the page.')
}
app.mount('#app')

View File

@@ -15,6 +15,7 @@ export const useAppStore = defineStore('app', () => {
const isLoading = ref(false)
const error = ref<string | null>(null)
let isWsSubscribed = false
let isWsConnecting = false
let sessionValidated = false
// Computed
@@ -86,10 +87,14 @@ export const useAppStore = defineStore('app', () => {
}
async function connectWebSocket(): Promise<void> {
// Prevent concurrent connection attempts
if (isWsConnecting) return
isWsConnecting = true
try {
if (import.meta.env.DEV) console.log('[Store] Connecting WebSocket...')
isReconnecting.value = true
// Don't create multiple subscriptions - check if already subscribed
if (!isWsSubscribed) {
// Subscribe to updates BEFORE connecting (so we catch initial data)
@@ -159,6 +164,8 @@ export const useAppStore = defineStore('app', () => {
isConnected.value = false
// Don't throw - allow app to work without real-time updates
// The WebSocket will reconnect in the background
} finally {
isWsConnecting = false
}
}

View File

@@ -118,6 +118,9 @@ export const useMeshStore = defineStore('mesh', () => {
const error = ref<string | null>(null)
const sending = ref(false)
// Serialize send operations to prevent concurrent fetchMessages() races
let sendQueue: Promise<void> = Promise.resolve()
// Node position tracking for map view (contact_id -> position)
const nodePositions = ref<Map<number, NodePosition>>(new Map())
@@ -247,24 +250,30 @@ export const useMeshStore = defineStore('mesh', () => {
}
async function sendMessage(contactId: number, message: string) {
try {
sending.value = true
error.value = null
const res = await rpcClient.call<{ sent: boolean; message_id: number; encrypted: boolean }>({
method: 'mesh.send',
params: { contact_id: contactId, message: message.trim() },
})
// Refresh messages after sending
if (res.sent) {
await fetchMessages()
const doSend = async () => {
try {
sending.value = true
error.value = null
const res = await rpcClient.call<{ sent: boolean; message_id: number; encrypted: boolean }>({
method: 'mesh.send',
params: { contact_id: contactId, message: message.trim() },
})
// Refresh messages after sending
if (res.sent) {
await fetchMessages()
}
return res
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to send mesh message'
throw err
} finally {
sending.value = false
}
return res
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to send mesh message'
throw err
} finally {
sending.value = false
}
// Chain onto send queue to prevent concurrent fetchMessages() calls
const result = sendQueue.then(doSend, doSend)
sendQueue = result.then(() => {}, () => {})
return result
}
async function broadcastIdentity() {