Newer
Older
smart-home-server / webclient-vue / src / features / devices / pages / DevicesListPage.vue
<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>