<template>
<section class="page">
<GnPageHeader title="Scanning" kicker="Devices">
<template #actions>
<GnButton
:loading="scanningStore.isLoading"
icon="ph-magnifying-glass"
@click="scan"
>
Scan
</GnButton>
</template>
</GnPageHeader>
<div class="scan-filters">
<span class="filter-label">Mode:</span>
<GnChip
:selected="scanningStore.mode === 'setup'"
clickable
icon="ph-plug"
@click="setMode('setup')"
>
Setup
</GnChip>
<GnChip
:selected="scanningStore.mode === 'all'"
clickable
icon="ph-scan"
@click="setMode('all')"
>
All
</GnChip>
</div>
<AppLoadingState v-if="scanningStore.isLoading" text="Scanning network" />
<AppErrorState
v-else-if="scanningStore.error"
title="Scan failed"
:error="scanningStore.error"
:retry="scan"
/>
<AppEmptyState
v-else-if="scanningStore.devices.length === 0"
title="No devices found"
message="Choose scan mode and click Scan to discover devices."
/>
<div v-else class="devices-panel">
<div class="devices-summary">
<GnBadge variant="primary">{{ scanningStore.total }} found</GnBadge>
</div>
<GnTable
:columns="tableColumns"
:rows="scanningStore.devices"
caption="Discovered devices"
>
<template #cell-device="{ row }">
<div class="device-cell">
<div class="device-icon" v-html="typeIcon(row.device_type)" />
<div class="device-info">
<strong>{{ row.device_name || 'Unknown' }}</strong>
<small>{{ row.device_type || 'unknown' }} — {{ row.ip_address || '—' }}</small>
</div>
</div>
</template>
<template #cell-status="{ row }">
<GnBadge :variant="row.status === 'setup' ? 'warning' : 'success'">{{ row.status || 'unknown' }}</GnBadge>
</template>
<template #cell-firmware="{ row }">
<span class="firmware">{{ row.firmware_version || '—' }}</span>
</template>
<template #cell-actions="{ row }">
<GnButton
v-if="row.status === 'setup'"
variant="primary"
icon="ph-plus"
size="sm"
@click="openSetup(row)"
>
Add
</GnButton>
<span v-else class="muted">—</span>
</template>
</GnTable>
</div>
<GnModal
:open="showSetupModal"
title="Setup new device"
@update:open="showSetupModal = $event"
>
<div class="form-group">
<GnInput v-model="setupForm.alias" label="Alias" placeholder="kitchen_relay" />
</div>
<div class="form-group">
<GnInput v-model="setupForm.name" label="Name" placeholder="Kitchen Relay" />
</div>
<div class="form-group">
<GnInput v-model="setupForm.description" label="Description" />
</div>
<div v-if="setupError" class="form-group">
<GnAlert variant="danger">{{ setupError }}</GnAlert>
</div>
<template #footer>
<GnButton variant="secondary" @click="showSetupModal = false">Cancel</GnButton>
<GnButton variant="primary" icon="ph-plus" :loading="setupLoading" @click="submitSetup">
Add device
</GnButton>
</template>
</GnModal>
</section>
</template>
<script setup>
import { ref, reactive } from "vue";
import { useScanningStore } from "../../../stores/scanning";
import {
GnPageHeader,
GnButton,
GnBadge,
GnChip,
GnTable,
GnModal,
GnInput,
GnAlert,
useToast,
} from "gnexus-ui-kit/vue";
import AppEmptyState from "../../../components/feedback/AppEmptyState.vue";
import AppErrorState from "../../../components/feedback/AppErrorState.vue";
import AppLoadingState from "../../../components/feedback/AppLoadingState.vue";
const scanningStore = useScanningStore();
const toast = useToast();
const showSetupModal = ref(false);
const setupLoading = ref(false);
const setupError = ref("");
const setupForm = reactive({
device_ip: "",
alias: "",
name: "",
description: "",
});
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 tableColumns = [
{ key: "device", label: "Device" },
{ key: "status", label: "Status" },
{ key: "firmware", label: "Firmware" },
{ key: "actions", label: "Actions" },
];
function typeIcon(type) {
return TYPE_ICONS[type] || TYPE_ICONS.relay;
}
function setMode(mode) {
scanningStore.setMode(mode);
}
function scan() {
scanningStore.scan();
}
function openSetup(row) {
setupForm.device_ip = row.ip_address;
setupForm.alias = "";
setupForm.name = "";
setupForm.description = "";
setupError.value = "";
showSetupModal.value = true;
}
async function submitSetup() {
setupLoading.value = true;
setupError.value = "";
const result = await scanningStore.setupDevice({ ...setupForm });
setupLoading.value = false;
if (!result.ok) {
setupError.value = result.error?.message || "Failed to setup device";
return;
}
showSetupModal.value = false;
toast.success({ title: "Device added", text: `Device ${setupForm.alias || setupForm.name || ""} added successfully` });
}
</script>
<style scoped>
.scan-filters {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 24px;
}
.filter-label {
color: var(--color-muted);
font-size: 13px;
text-transform: uppercase;
}
.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;
}
.firmware {
font-size: 12px;
color: var(--color-muted);
}
.muted {
color: var(--color-muted);
}
.form-group {
margin-bottom: 16px;
}
</style>