<template>
<section class="page">
<GnPageHeader title="Device Matrix" kicker="Devices">
<template #actions>
<GnButton
:loading="devicesStore.isLoading || devicesStore.isLoadingStates"
icon="ph-arrow-clockwise"
@click="reload"
>
Refresh
</GnButton>
</template>
</GnPageHeader>
<AppLoadingState v-if="devicesStore.isLoading" text="Loading devices" />
<AppErrorState
v-else-if="devicesStore.error"
title="Devices loading failed"
:message="devicesStore.error.message"
:retry="reload"
/>
<AppEmptyState
v-else-if="devicesStore.devices.length === 0"
title="No active devices"
message="No active devices found."
/>
<div v-else class="devices-panel">
<div class="devices-summary">
<GnBadge variant="primary">Total: {{ devicesStore.total }}</GnBadge>
<GnBadge v-if="devicesStore.isLoadingStates" variant="secondary">States loading</GnBadge>
<GnBadge v-else variant="success">States settled</GnBadge>
<GnBadge v-if="isStale" variant="warning">Stale data</GnBadge>
</div>
<AppErrorState
v-if="devicesStore.stateError"
title="Device states loading failed"
:message="devicesStore.stateError.message"
/>
<GnTable
:columns="tableColumns"
:rows="tableRows"
caption="Registered devices"
>
<template #cell-device_name="{ row }">
<strong>{{ row.device_name }}</strong>
<small>{{ row.alias }}</small>
</template>
<template #cell-connect="{ row }">
<DeviceConnectionBadge :status="row.connectionStatus" />
</template>
<template #cell-state="{ row }">
<DeviceStateCell :state="row.state" />
</template>
<template #cell-actions="{ row }">
<div class="devices-actions">
<GnButton variant="secondary" icon="ph-info" disabled>Details</GnButton>
<GnButton
variant="warning"
icon="ph-arrow-clockwise"
:loading="devicesStore.isRebooting(row.id)"
@click="reboot(row.id)"
>
Reboot
</GnButton>
</div>
</template>
</GnTable>
</div>
</section>
</template>
<script setup>
import { onMounted, computed } from "vue";
import { useDevicesStore } from "../../../stores/devices";
import { GnPageHeader, GnButton, GnBadge, GnTable } from "gnexus-ui-kit/vue";
const STALE_THRESHOLD_MS = 5 * 60 * 1000;
import AppEmptyState from "../../../components/feedback/AppEmptyState.vue";
import AppErrorState from "../../../components/feedback/AppErrorState.vue";
import AppLoadingState from "../../../components/feedback/AppLoadingState.vue";
import DeviceConnectionBadge from "../components/DeviceConnectionBadge.vue";
import DeviceStateCell from "../components/DeviceStateCell.vue";
const devicesStore = useDevicesStore();
const isStale = computed(() => {
if (!devicesStore.lastLoadedAt) return false;
const loaded = new Date(devicesStore.lastLoadedAt).getTime();
return Date.now() - loaded > STALE_THRESHOLD_MS;
});
const tableColumns = [
{ key: "device_name", label: "Device name" },
{ key: "connect", label: "Connect" },
{ key: "state", label: "State" },
{ key: "ip", label: "IP" },
{ key: "type", label: "Type" },
{ key: "actions", label: "Actions" },
];
const tableRows = computed(() =>
devicesStore.devices.map((device) => {
const st = stateFor(device);
return {
id: device.id,
device_name: device.name || device.alias || `Device #${device.id}`,
alias: device.alias,
connectionStatus: st.connectionStatus,
state: st,
ip: device.device_ip || "unknown",
type: device.device_type || "unknown",
};
})
);
function stateFor(device) {
return (
devicesStore.stateByDeviceId[String(device.id)] || {
status: "idle",
message: "Not loaded",
connectionStatus: device.connection_status || "unknown",
}
);
}
async function reload() {
const result = await devicesStore.loadDevices();
if (result.ok) {
await devicesStore.loadDeviceStates();
}
}
function reboot(id) {
devicesStore.rebootDevice(id);
}
onMounted(reload);
</script>