<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="devicesStore.devices"
caption="Registered devices"
>
<template #cell-device="{ row }">
<router-link
:to="{ name: 'device-detail', params: { id: String(row.id) } }"
class="device-link"
>
<div class="device-cell">
<div class="device-icon" v-html="typeIcon(row.device_type)" />
<div class="device-info">
<strong>{{ row.name || row.alias || `Device #${row.id}` }}</strong>
<small>{{ row.alias }} — {{ row.device_ip || '—' }}</small>
</div>
</div>
</router-link>
</template>
<template #cell-connect="{ row }">
<DeviceConnectionBadge :status="row.connection_status || 'unknown'" />
</template>
<template #cell-state="{ row }">
<DeviceStateCell :state="stateFor(row)" />
</template>
<template #cell-actions="{ row }">
<GnButton
variant="warning"
icon="ph-arrow-clockwise"
size="sm"
:loading="devicesStore.isRebooting(row.id)"
@click="reboot(row.id)"
>
Reboot
</GnButton>
</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 TYPE_ICONS = {
relay: '<i class="ph ph-toggle-left"></i>',
button: '<i class="ph ph-hand-tap"></i>',
sensor: '<i class="ph ph-thermometer"></i>',
};
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", label: "Device" },
{ key: "connect", label: "Connect" },
{ key: "state", label: "State" },
{ key: "actions", label: "Actions" },
];
function typeIcon(type) {
return TYPE_ICONS[type] || TYPE_ICONS.relay;
}
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>
<style scoped>
.devices-summary {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.device-cell {
display: flex;
align-items: center;
gap: 10px;
}
.device-icon {
font-size: 20px;
color: var(--color-primary);
}
.device-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.device-info small {
color: var(--color-muted);
font-size: 12px;
}
.device-link {
color: inherit;
text-decoration: none;
}
.device-link:hover strong {
color: var(--color-primary);
}
</style>