Newer
Older
smart-home-server / webclient / src / features / firmwares / pages / FirmwaresListPage.vue
<template>
  <section class="page">
    <GnPageHeader title="Firmware Catalog" kicker="Firmwares">
      <template #actions>
        <GnButton
          :loading="firmwaresStore.isUpdating"
          icon="ph-arrow-clockwise"
          @click="handleRefresh"
        >
          Refresh Catalog
        </GnButton>
      </template>
    </GnPageHeader>

    <AppLoadingState v-if="firmwaresStore.isLoadingList" text="Loading firmware catalog" />

    <AppErrorState
      v-else-if="error"
      title="Catalog loading failed"
      :error="error"
      :retry="reload"
    />

    <AppEmptyState
      v-else-if="firmwaresStore.firmwares.length === 0"
      title="No firmwares found"
      message="No firmware packages detected in the firmwares directory."
    />

    <div v-else class="firmwares-panel">
      <div class="firmwares-summary">
        <GnBadge variant="primary">Total: {{ firmwaresStore.firmwares.length }}</GnBadge>
      </div>

      <div class="firmwares-list">
        <div
          v-for="fw in firmwaresStore.firmwares"
          :key="fw.id"
          class="firmware-card"
        >
          <div class="firmware-header">
            <span class="firmware-id">{{ fw.id }}</span>
            <GnBadge variant="success">{{ fw.version }}</GnBadge>
          </div>

          <div class="firmware-meta">
            <GnBadge variant="secondary">{{ fw.device_type }}</GnBadge>
            <GnBadge v-if="fw.platform" variant="info">{{ fw.platform }}</GnBadge>
            <GnBadge v-if="fw.channels" variant="warning">{{ fw.channels }} ch</GnBadge>
          </div>

          <p v-if="fw.description" class="firmware-desc">{{ fw.description }}</p>
          <pre v-if="fw.changelog" class="firmware-changelog">{{ fw.changelog }}</pre>
        </div>
      </div>
    </div>
  </section>
</template>

<script setup>
import { ref, onMounted } from "vue";
import { useFirmwaresStore } from "../../../stores/firmwares";
import { GnPageHeader, GnButton, GnBadge } 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 firmwaresStore = useFirmwaresStore();
const error = ref(null);

async function reload() {
  error.value = null;
  const result = await firmwaresStore.loadFirmwares();
  if (!result.ok) {
    error.value = result.error?.message || "Failed to load catalog";
  }
}

async function handleRefresh() {
  error.value = null;
  const result = await firmwaresStore.refreshFirmwares();
  if (!result.ok) {
    error.value = result.error?.message || "Failed to refresh catalog";
  }
}

onMounted(reload);
</script>

<style scoped>
.firmwares-summary {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 12px;
}

.firmwares-list {
  display: grid;
  gap: 12px;
}

.firmware-card {
  background: var(--color-panel);
  border: 1px solid rgba(192, 202, 245, 0.12);
  border-radius: 8px;
  padding: 12px 16px;
}

.firmware-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
}

.firmware-id {
  font-family: monospace;
  font-size: 13px;
  word-break: break-all;
}

.firmware-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 8px;
}

.firmware-desc {
  font-size: 13px;
  color: var(--color-text-muted);
  margin: 0;
}

.firmware-changelog {
  font-size: 12px;
  background: rgba(15, 23, 42, 0.5);
  padding: 8px;
  border-radius: 6px;
  margin-top: 8px;
  white-space: pre-wrap;
  word-break: break-word;
}
</style>