diff --git a/webclient/src/api/modules/firmwares.js b/webclient/src/api/modules/firmwares.js new file mode 100644 index 0000000..0eaf5af --- /dev/null +++ b/webclient/src/api/modules/firmwares.js @@ -0,0 +1,30 @@ +import { apiGet, apiPost } from "../client"; + +function safeId(id) { + return encodeURIComponent(String(id)); +} + +export const firmwaresApi = { + async list() { + return apiGet("/api/v1/firmwares"); + }, + + async detail(id) { + return apiGet(`/api/v1/firmwares/id/${safeId(id)}`); + }, + + async refresh() { + return apiPost("/api/v1/firmwares/refresh"); + }, + + async deviceCompatibility(deviceId) { + return apiGet(`/api/v1/devices/id/${safeId(deviceId)}/firmware-compatibility`); + }, + + async updateDeviceFirmware(deviceId, firmwareId) { + return apiPost("/api/v1/devices/update-firmware", { + device_id: deviceId, + firmware_id: firmwareId, + }); + }, +}; diff --git a/webclient/src/components/layout/AppShell.vue b/webclient/src/components/layout/AppShell.vue index c7dcc6d..869dcd1 100644 --- a/webclient/src/components/layout/AppShell.vue +++ b/webclient/src/components/layout/AppShell.vue @@ -32,6 +32,7 @@ "scripts-regular": "Regular", "scripts-scopes": "Scopes", "script-detail": "Script", + firmwares: "Firmwares", }; const pageTitle = computed(() => { @@ -50,5 +51,6 @@ { label: "Actions", to: "/scripts/actions", icon: "ph-play" }, { label: "Regular", to: "/scripts/regular", icon: "ph-clock" }, { label: "Scopes", to: "/scripts/scopes", icon: "ph-brackets-curly" }, + { label: "Firmwares", to: "/firmwares", icon: "ph-cloud-arrow-down" }, ]; diff --git a/webclient/src/features/devices/pages/DeviceDetailPage.vue b/webclient/src/features/devices/pages/DeviceDetailPage.vue index b6f5e16..087e7ca 100644 --- a/webclient/src/features/devices/pages/DeviceDetailPage.vue +++ b/webclient/src/features/devices/pages/DeviceDetailPage.vue @@ -78,6 +78,17 @@ @assign="openAssign" /> +
+
Firmware Update
+ + New firmware available: + {{ fw.version }} + + + Update Firmware + +
+
Description

{{ device.description }}

@@ -205,6 +216,41 @@ + + +

Select firmware to install on {{ device?.name || device?.alias }}:

+
+
+
{{ fw.version }}
+
{{ fw.description }}
+
+
+
+ {{ firmwareUpdateError }} +
+ +
@@ -213,6 +259,7 @@ import { useRoute, useRouter } from "vue-router"; import { useDevicesStore } from "../../../stores/devices"; import { useAreasStore } from "../../../stores/areas"; +import { useFirmwaresStore } from "../../../stores/firmwares"; import { useAreaAssign } from "../../../composables/useAreaAssign"; import { GnPageHeader, @@ -236,6 +283,7 @@ const router = useRouter(); const devicesStore = useDevicesStore(); const areasStore = useAreasStore(); +const firmwaresStore = useFirmwaresStore(); const toast = useToast(); const { @@ -288,6 +336,18 @@ const resetLoading = ref(false); const resetError = ref(""); +const showFirmwareUpdateModal = ref(false); +const selectedFirmwareId = ref(null); +const firmwareUpdateError = ref(""); + +const hasCompatibleFirmwares = computed(() => { + return (firmwaresStore.compatibility?.compatible?.length || 0) > 0; +}); + +const compatibleFirmwares = computed(() => { + return firmwaresStore.compatibility?.compatible || []; +}); + const deviceActions = computed(() => { const actions = [{ label: "Edit", icon: "ph-pencil", onSelect: openEdit }]; @@ -470,6 +530,24 @@ router.push({ name: "devices" }); } +function openFirmwareUpdate() { + selectedFirmwareId.value = compatibleFirmwares.value[0]?.id || null; + firmwareUpdateError.value = ""; + showFirmwareUpdateModal.value = true; +} + +async function submitFirmwareUpdate() { + if (!selectedFirmwareId.value || !device.value) return; + const result = await firmwaresStore.updateDeviceFirmware(device.value.id, selectedFirmwareId.value); + if (!result.ok) { + firmwareUpdateError.value = result.error?.message || "Firmware update failed"; + return; + } + showFirmwareUpdateModal.value = false; + toast.success({ title: "Updated", text: "Firmware update pushed successfully" }); + await devicesStore.loadDeviceDetail(deviceId.value); +} + function systemStatusVariant(status) { const map = { active: "success", @@ -509,6 +587,7 @@ if (device.value?.connection_status === "active") { await loadStatus(); + await firmwaresStore.loadDeviceCompatibility(id); } } @@ -518,6 +597,7 @@ onUnmounted(() => { devicesStore.clearDeviceDetail(); + firmwaresStore.clearCompatibility(); }); watch(() => route.params.id, (newId, oldId) => { @@ -577,4 +657,34 @@ .form-group { margin-bottom: 16px; } + +.firmware-options { + display: grid; + gap: 8px; + margin: 12px 0; +} + +.firmware-option { + padding: 10px 12px; + border: 1px solid rgba(192, 202, 245, 0.12); + border-radius: 6px; + cursor: pointer; + background: var(--color-panel); +} + +.firmware-option.active { + border-color: var(--color-primary); + background: rgba(59, 130, 246, 0.1); +} + +.fw-version { + font-weight: 600; + font-size: 13px; +} + +.fw-desc { + font-size: 12px; + color: var(--color-text-muted); + margin-top: 2px; +} diff --git a/webclient/src/features/firmwares/pages/FirmwaresListPage.vue b/webclient/src/features/firmwares/pages/FirmwaresListPage.vue new file mode 100644 index 0000000..2c153ef --- /dev/null +++ b/webclient/src/features/firmwares/pages/FirmwaresListPage.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/webclient/src/router/routes.js b/webclient/src/router/routes.js index 41fe5fc..63c423a 100644 --- a/webclient/src/router/routes.js +++ b/webclient/src/router/routes.js @@ -8,6 +8,7 @@ import ScriptsRegularPage from "../features/scripts/pages/ScriptsRegularPage.vue"; import ScriptsScopesPage from "../features/scripts/pages/ScriptsScopesPage.vue"; import ScriptDetailPage from "../features/scripts/pages/ScriptDetailPage.vue"; +import FirmwaresListPage from "../features/firmwares/pages/FirmwaresListPage.vue"; export const routes = [ { @@ -65,6 +66,11 @@ component: ScriptDetailPage, }, { + path: "/firmwares", + name: "firmwares", + component: FirmwaresListPage, + }, + { path: "/:pathMatch(.*)*", name: "not-found", component: () => import("../features/system/NotFoundPage.vue"), diff --git a/webclient/src/stores/firmwares.js b/webclient/src/stores/firmwares.js new file mode 100644 index 0000000..36169a9 --- /dev/null +++ b/webclient/src/stores/firmwares.js @@ -0,0 +1,89 @@ +import { ref, computed } from "vue"; +import { defineStore } from "pinia"; +import { firmwaresApi } from "../api/modules/firmwares"; +import { useAsyncRequest } from "../composables/useAsyncRequest"; + +export const useFirmwaresStore = defineStore("firmwares", () => { + const firmwares = ref([]); + const current = ref(null); + const compatibility = ref(null); + + const listRequest = useAsyncRequest(); + const detailRequest = useAsyncRequest(); + const actionRequest = useAsyncRequest(); + const compatibilityRequest = useAsyncRequest(); + + const isLoadingList = computed(() => listRequest.isLoading.value); + const isLoadingDetail = computed(() => detailRequest.isLoading.value); + const isUpdating = computed(() => actionRequest.isLoading.value); + const isLoadingCompatibility = computed(() => compatibilityRequest.isLoading.value); + + async function loadFirmwares() { + return listRequest.execute(async (signal) => { + const result = await firmwaresApi.list({ signal }); + if (result.ok) { + firmwares.value = result.data?.data?.firmwares || []; + } + return result; + }); + } + + async function refreshFirmwares() { + return actionRequest.execute(async () => { + const result = await firmwaresApi.refresh(); + if (result.ok) { + await loadFirmwares(); + } + return result; + }); + } + + async function loadFirmwareDetail(id) { + return detailRequest.execute(async (signal) => { + const result = await firmwaresApi.detail(id, { signal }); + if (result.ok) { + current.value = result.data?.data?.firmware || null; + } + return result; + }); + } + + async function loadDeviceCompatibility(deviceId) { + return compatibilityRequest.execute(async (signal) => { + const result = await firmwaresApi.deviceCompatibility(deviceId, { signal }); + if (result.ok) { + compatibility.value = { + compatible: result.data?.data?.compatible || [], + currentVersion: result.data?.data?.current_version || "unknown", + }; + } + return result; + }); + } + + async function updateDeviceFirmware(deviceId, firmwareId) { + return actionRequest.execute(async () => { + return firmwaresApi.updateDeviceFirmware(deviceId, firmwareId); + }); + } + + function clearCompatibility() { + compatibility.value = null; + } + + return { + firmwares, + current, + compatibility, + isLoadingList, + isLoadingDetail, + isUpdating, + isLoadingCompatibility, + loadFirmwares, + refreshFirmwares, + loadFirmwareDetail, + loadDeviceCompatibility, + updateDeviceFirmware, + clearCompatibility, + }; +});