diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js new file mode 100644 index 0000000..6931bcc --- /dev/null +++ b/frontend/src/api/index.js @@ -0,0 +1,46 @@ +const BASE_URL = '/api' + +async function request(path, options = {}) { + const url = `${BASE_URL}${path}` + const config = { + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + ...options, + } + if (config.body && typeof config.body === 'object') { + config.body = JSON.stringify(config.body) + } + const res = await fetch(url, config) + if (!res.ok) { + const error = new Error(`API error: ${res.status}`) + error.status = res.status + try { + error.data = await res.json() + } catch { + error.data = await res.text() + } + throw error + } + if (res.status === 204) return null + return res.json() +} + +// Devices +export const getDevices = () => request('/devices') +export const createDevice = (data) => request('/devices', { method: 'POST', body: data }) +export const getDevice = (id) => request(`/devices/${id}`) +export const updateDevice = (id, data) => request(`/devices/${id}`, { method: 'PUT', body: data }) +export const deleteDevice = (id) => request(`/devices/${id}`, { method: 'DELETE' }) +export const sendCommand = (id, payload) => + request(`/devices/${id}/command`, { method: 'POST', body: { payload } }) +export const getDeviceLogs = (id, limit = 20) => request(`/devices/${id}/logs?limit=${limit}`) + +// Broker +export const getBrokerStatus = () => request('/broker/status') +export const getBrokerClients = () => request('/broker/clients') +export const getBrokerTopics = () => request('/broker/topics') + +// Dashboard +export const getDashboardStats = () => request('/dashboard') diff --git a/frontend/src/composables/useWebSocket.js b/frontend/src/composables/useWebSocket.js new file mode 100644 index 0000000..f6f72d3 --- /dev/null +++ b/frontend/src/composables/useWebSocket.js @@ -0,0 +1,48 @@ +import { ref, onMounted, onUnmounted } from 'vue' + +export function useWebSocket(url, onMessage) { + const connected = ref(false) + let ws = null + let reconnectTimer = null + let retryDelay = 1000 + + function connect() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const fullUrl = `${protocol}//${window.location.host}${url}` + + ws = new WebSocket(fullUrl) + + ws.onopen = () => { + connected.value = true + retryDelay = 1000 + } + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data) + onMessage(data) + } catch { + // ignore non-JSON messages + } + } + + ws.onclose = () => { + connected.value = false + reconnectTimer = setTimeout(connect, retryDelay) + retryDelay = Math.min(retryDelay * 2, 30000) + } + + ws.onerror = () => { + ws.close() + } + } + + onMounted(() => connect()) + + onUnmounted(() => { + clearTimeout(reconnectTimer) + if (ws) ws.close() + }) + + return { connected } +} diff --git a/frontend/src/stores/devices.js b/frontend/src/stores/devices.js new file mode 100644 index 0000000..8ab1a0b --- /dev/null +++ b/frontend/src/stores/devices.js @@ -0,0 +1,60 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { getDevices, createDevice, deleteDevice, sendCommand as apiSendCommand } from '../api' +import { useWebSocket } from '../composables/useWebSocket' + +export const useDeviceStore = defineStore('devices', () => { + const devices = ref([]) + const loading = ref(false) + const wsConnected = ref(false) + + async function fetchDevices() { + loading.value = true + try { + devices.value = await getDevices() + } finally { + loading.value = false + } + } + + async function addDevice(data) { + const device = await createDevice(data) + devices.value.unshift(device) + return device + } + + async function removeDevice(id) { + await deleteDevice(id) + devices.value = devices.value.filter((d) => d.id !== id) + } + + async function sendCommand(id, payload) { + return await apiSendCommand(id, payload) + } + + function handleWsMessage(data) { + if (data.type !== 'device_update') return + const idx = devices.value.findIndex((d) => d.id === data.device_id) + if (idx === -1) return + const device = devices.value[idx] + devices.value.splice(idx, 1, { + ...device, + state: data.state, + is_online: data.is_online, + last_seen: data.last_seen, + }) + } + + // Initialize WebSocket + const { connected } = useWebSocket('/ws/devices', handleWsMessage) + + return { + devices, + loading, + wsConnected, + fetchDevices, + addDevice, + removeDevice, + sendCommand, + } +})