import { defineStore } from 'pinia' import { ref } from 'vue' import { systemApi, streamApi, hidApi, atxApi, msdApi, type DeviceInfo } from '@/api' interface SystemCapabilities { video: { available: boolean; backend?: string } hid: { available: boolean; backend?: string } msd: { available: boolean } atx: { available: boolean; backend?: string } audio: { available: boolean; backend?: string } } interface DiskSpaceInfo { total: number available: number used: number } interface StreamState { online: boolean active: boolean device: string | null format: string | null resolution: [number, number] | null targetFps: number clients: number streamMode: string // 'mjpeg' or 'webrtc' error: string | null } interface HidState { available: boolean backend: string initialized: boolean online: boolean supportsAbsoluteMouse: boolean keyboardLedsEnabled: boolean ledState: { numLock: boolean capsLock: boolean scrollLock: boolean } device: string | null error: string | null errorCode: string | null } interface AtxState { available: boolean backend: string initialized: boolean powerOn: boolean error: string | null } interface MsdState { available: boolean connected: boolean mode: 'none' | 'image' | 'drive' imageId: string | null error: string | null } interface AudioState { available: boolean streaming: boolean device: string | null quality: string error: string | null } interface ConnectionState { wsConnected: boolean hidWsConnected: boolean wsNetworkError: boolean hidWsNetworkError: boolean } // DeviceInfo event payload types (from WebSocket) export interface VideoDeviceInfo { available: boolean device: string | null format: string | null resolution: [number, number] | null fps: number online: boolean stream_mode: string // 'mjpeg' or 'webrtc' config_changing?: boolean error: string | null } export interface HidDeviceInfo { available: boolean backend: string initialized: boolean online: boolean supports_absolute_mouse: boolean keyboard_leds_enabled: boolean led_state: { num_lock: boolean caps_lock: boolean scroll_lock: boolean compose: boolean kana: boolean } device: string | null error: string | null error_code?: string | null } export interface MsdDeviceInfo { available: boolean mode: string connected: boolean image_id: string | null error: string | null } export interface AtxDeviceInfo { available: boolean backend: string initialized: boolean power_on: boolean error: string | null } export interface AudioDeviceInfo { available: boolean streaming: boolean device: string | null quality: string error: string | null } export interface TtydDeviceInfo { available: boolean running: boolean } export interface DeviceInfoEvent { video: VideoDeviceInfo hid: HidDeviceInfo msd: MsdDeviceInfo | null atx: AtxDeviceInfo | null audio: AudioDeviceInfo | null ttyd: TtydDeviceInfo } export const useSystemStore = defineStore('system', () => { const version = ref('') const buildDate = ref('') const capabilities = ref(null) const diskSpace = ref(null) const deviceInfo = ref(null) const stream = ref(null) const hid = ref(null) const atx = ref(null) const msd = ref(null) const audio = ref(null) const loading = ref(false) const error = ref(null) const connectionState = ref({ wsConnected: false, hidWsConnected: false, wsNetworkError: false, hidWsNetworkError: false, }) async function fetchSystemInfo() { try { const info = await systemApi.info() version.value = info.version buildDate.value = info.build_date capabilities.value = info.capabilities diskSpace.value = info.disk_space ?? null deviceInfo.value = info.device_info ?? null return info } catch (e) { console.error('Failed to fetch system info:', e) throw e } } async function startStream() { try { await streamApi.start() } catch (e) { console.error('Failed to start stream:', e) throw e } } async function stopStream() { try { await streamApi.stop() } catch (e) { console.error('Failed to stop stream:', e) throw e } } async function fetchHidState() { try { const state = await hidApi.status() hid.value = { available: state.available, backend: state.backend, initialized: state.initialized, online: state.online, supportsAbsoluteMouse: state.supports_absolute_mouse, keyboardLedsEnabled: state.keyboard_leds_enabled, ledState: { numLock: state.led_state.num_lock, capsLock: state.led_state.caps_lock, scrollLock: state.led_state.scroll_lock, }, device: state.device ?? null, error: state.error ?? null, errorCode: state.error_code ?? null, } return state } catch (e) { console.error('Failed to fetch HID state:', e) throw e } } async function fetchAtxState() { try { const state = await atxApi.status() atx.value = { available: state.available, backend: state.backend, initialized: state.initialized, powerOn: state.power_status === 'on', error: null, } return state } catch (e) { console.error('Failed to fetch ATX state:', e) throw e } } async function fetchMsdState() { try { const result = await msdApi.status() msd.value = { available: result.available, connected: result.state.connected, mode: result.state.mode, imageId: result.state.current_image?.id ?? null, error: null, } return result } catch (e) { console.error('Failed to fetch MSD state:', e) throw e } } async function fetchStreamState() { try { const [status, modeResp] = await Promise.all([ streamApi.status(), streamApi.getMode().catch(() => ({ mode: 'mjpeg' })) ]) stream.value = { online: status.state === 'streaming', active: status.state !== 'uninitialized', device: status.device, format: status.format, resolution: status.resolution, targetFps: status.target_fps, clients: status.clients, streamMode: modeResp.mode || 'mjpeg', error: status.state === 'error' ? 'Stream error' : null, } return status } catch (e) { console.error('Failed to fetch stream state:', e) throw e } } async function fetchAllStates() { loading.value = true error.value = null try { await Promise.all([ fetchSystemInfo(), fetchStreamState().catch(() => null), fetchHidState().catch(() => null), fetchAtxState().catch(() => null), fetchMsdState().catch(() => null), ]) } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to fetch states' } finally { loading.value = false } } /** * Update WebSocket connection state */ function updateWsConnection(connected: boolean, networkError: boolean) { connectionState.value.wsConnected = connected connectionState.value.wsNetworkError = networkError } /** * Update HID WebSocket connection state */ function updateHidWsConnection(connected: boolean, networkError: boolean) { connectionState.value.hidWsConnected = connected connectionState.value.hidWsNetworkError = networkError } /** * Update store state from WebSocket DeviceInfo event * Called when receiving system.device_info event from server */ function updateFromDeviceInfo(data: DeviceInfoEvent) { // Update video/stream state stream.value = { online: data.video.online, active: data.video.online || data.video.available, device: data.video.device, format: data.video.format, resolution: data.video.resolution, targetFps: data.video.fps, clients: stream.value?.clients ?? 0, streamMode: data.video.stream_mode || 'mjpeg', error: data.video.error, } // Update HID state hid.value = { available: data.hid.available, backend: data.hid.backend, initialized: data.hid.initialized, online: data.hid.online, supportsAbsoluteMouse: data.hid.supports_absolute_mouse, keyboardLedsEnabled: data.hid.keyboard_leds_enabled, ledState: { numLock: data.hid.led_state.num_lock, capsLock: data.hid.led_state.caps_lock, scrollLock: data.hid.led_state.scroll_lock, }, device: data.hid.device, error: data.hid.error, errorCode: data.hid.error_code ?? null, } // Update MSD state (optional) if (data.msd) { msd.value = { available: data.msd.available, connected: data.msd.connected, mode: data.msd.mode as 'none' | 'image' | 'drive', imageId: data.msd.image_id, error: data.msd.error, } } else { msd.value = null } // Update ATX state (optional) if (data.atx) { atx.value = { available: data.atx.available, backend: data.atx.backend, initialized: data.atx.initialized, powerOn: data.atx.power_on, error: data.atx.error, } } else { atx.value = null } // Update Audio state (optional) if (data.audio) { audio.value = { available: data.audio.available, streaming: data.audio.streaming, device: data.audio.device, quality: data.audio.quality, error: data.audio.error, } } else { audio.value = null } } /** * Update stream clients count from WebSocket stream.stats_update event */ function updateStreamClients(clients: number) { if (stream.value) { stream.value = { ...stream.value, clients, } } } /** * Set stream online status * Called when MJPEG video successfully loads (frontend-side detection) * This fixes the timing issue where device_info event arrives before stream is fully active */ function setStreamOnline(online: boolean) { if (stream.value && stream.value.online !== online) { console.log('[Store] setStreamOnline:', online, '(was:', stream.value.online, ')') stream.value = { ...stream.value, online, } } } return { version, buildDate, capabilities, diskSpace, deviceInfo, stream, hid, atx, msd, audio, loading, error, connectionState, fetchSystemInfo, startStream, stopStream, fetchHidState, fetchAtxState, fetchMsdState, fetchAllStates, updateWsConnection, updateHidWsConnection, updateFromDeviceInfo, updateStreamClients, setStreamOnline, } })