internal/dashboard/frontend/src/views/StatsView.vue

<script setup lang="ts">
import { IconDatabase, IconGlobe, IconRefresh } from "@tabler/icons-vue";
import DetailCard from "components/DetailView/DetailCard.vue";
import LoaderOverlay from "components/LoaderOverlay.vue";
import PageHeader from "components/PageHeader.vue";
import SectionCard from "components/SectionCard.vue";
import { formatLocalDateTime, formatLocalNumber } from "utils/formatting";
import { onMounted, ref } from "vue";
import { icons } from "../utils/icons";

const systemStats = ref<any>(null);
const activeHoneypots = ref<any[]>([]);
const isUpdatingGeoDB = ref(false);
const updateMessage = ref("");

const fetchActiveHoneypots = async () => {
  try {
    const res = await fetch("/api/active-honeypots");
    if (res.ok) {
      activeHoneypots.value = await res.json();
    }
  } catch (e) {
    console.error("Failed to fetch active honeypots", e);
  }
};

const fetchSystemStats = async () => {
  try {
    const res = await fetch("/api/system-stats");
    if (res.ok) {
      systemStats.value = await res.json();
    }
  } catch (e) {
    console.error("Failed to fetch system stats", e);
  }
};

const formatSize = (bytes: number) => {
  if (!bytes) return "0 B";
  const k = 1024;
  const sizes = ["B", "KB", "MB", "GB", "TB"];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};

const updateGeoDB = async () => {
  if (isUpdatingGeoDB.value) return;
  isUpdatingGeoDB.value = true;
  updateMessage.value = "";
  try {
    const res = await fetch("/api/system/update-geodb", { method: "POST" });
    if (res.ok) {
      updateMessage.value = "GeoLite database updated successfully";
      await fetchSystemStats();
    } else {
      updateMessage.value = "Failed to update GeoLite database";
    }
  } catch (e) {
    console.error("Failed to update GeoDB", e);
    updateMessage.value = "Error updating GeoLite database";
  } finally {
    isUpdatingGeoDB.value = false;
  }
};

onMounted(() => {
  fetchSystemStats();
  fetchActiveHoneypots();
});
</script>

<template>
  <div class="page-container">
    <PageHeader title="System Statistics" :icon="icons.activity" />

    <div v-if="systemStats" class="grid items-start gap-8 md:grid-cols-2">
      <!-- Left Column -->
      <div class="grid gap-8">
        <!-- Active Honeypots -->
        <SectionCard title="Active Honeypots" :icon="icons.honeypotType">
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
            <div
              v-for="hp in activeHoneypots"
              :key="hp.name"
              class="flex flex-col rounded-lg border border-stone-800 bg-stone-900 p-4"
            >
              <div class="flex items-center gap-2 text-stone-100">
                <span class="font-bold">{{ hp.label }}</span>
                <span class="text-muted font-mono text-xs"
                  >({{ hp.name }})</span
                >
              </div>
              <div class="mt-2 flex flex-wrap gap-1">
                <div
                  v-if="!hp.ports || hp.ports.length === 0"
                  class="text-muted text-xs italic"
                >
                  No active ports (passive)
                </div>
                <div v-else class="flex items-center gap-2">
                  <span class="text-muted text-xs">Ports:</span>
                  <div
                    v-for="port in hp.ports"
                    :key="port"
                    class="rounded bg-stone-800 px-2 py-0.5 font-mono text-xs font-semibold text-stone-100"
                  >
                    {{ port }}
                  </div>
                </div>
              </div>
            </div>
          </div>
        </SectionCard>
        <!-- Database Statistics -->
        <SectionCard title="Database" :icon="IconDatabase">
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
            <DetailCard label="Total Size" class="bg-stone-900">
              <div class="mt-1 font-mono text-2xl text-stone-100">
                {{ formatSize(systemStats.database.database_size) }}
              </div>
            </DetailCard>
            <DetailCard label="Total Events" class="bg-stone-900">
              <div class="mt-1 font-mono text-2xl text-stone-100">
                {{ formatLocalNumber(systemStats.database.rows_events) }}
              </div>
            </DetailCard>
            <DetailCard label="Tracked IPs" class="bg-stone-900">
              <div class="mt-1 font-mono text-2xl text-stone-100">
                {{ formatLocalNumber(systemStats.database.rows_ips) }}
              </div>
            </DetailCard>
            <DetailCard label="Blocklist Entries" class="bg-stone-900">
              <div class="mt-1 font-mono text-2xl text-stone-100">
                {{ formatLocalNumber(systemStats.database.rows_blocklist) }}
              </div>
            </DetailCard>
            <DetailCard label="Unresolved IPs" class="bg-stone-900">
              <div class="mt-1 font-mono text-2xl text-stone-100">
                {{
                  formatLocalNumber(systemStats.database.rows_unresolved_ips)
                }}
              </div>
            </DetailCard>
          </div>
        </SectionCard>
      </div>

      <!-- Right Column -->
      <div class="grid gap-8">
        <!-- Updater Statistics -->
        <SectionCard title="Tickers & Caches" :icon="icons.time">
          <div class="space-y-4">
            <DetailCard label="IP Info Updater" class="bg-stone-900">
              <div class="mt-4 text-stone-400">Last successful run</div>
              <div class="font-mono text-lg">
                {{ formatLocalDateTime(systemStats.ip_info_last_run) }}
              </div>
            </DetailCard>
            <DetailCard label="Score Cache" class="bg-stone-900">
              <div class="mt-4">
                <div class="text-stone-400">Last updated</div>
              </div>
              <div class="font-mono text-lg">
                {{ formatLocalDateTime(systemStats.score_cache_updated) }}
              </div>
            </DetailCard>
          </div>
        </SectionCard>
        <!-- GeoLite Databases -->
        <SectionCard title="GeoLite Databases" :icon="IconGlobe">
          <div class="space-y-4">
            <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
              <DetailCard label="ASN Database" class="bg-stone-900">
                <div class="mt-4 text-stone-400">Last updated</div>
                <div class="font-mono text-lg">
                  {{ formatLocalDateTime(systemStats.geolite_asn_date) }}
                </div>
              </DetailCard>
              <DetailCard label="City Database" class="bg-stone-900">
                <div class="mt-4 text-stone-400">Last updated</div>
                <div class="font-mono text-lg">
                  {{ formatLocalDateTime(systemStats.geolite_city_date) }}
                </div>
              </DetailCard>
            </div>

            <div class="mt-8 flex flex-col items-center gap-4">
              <button
                @click="updateGeoDB"
                :disabled="isUpdatingGeoDB || !systemStats.geolite_urls_set"
                :title="
                  !systemStats.geolite_urls_set
                    ? 'Download URLs not configured'
                    : ''
                "
                class="btn-secondary px-4 py-2 text-sm disabled:opacity-50"
              >
                <IconRefresh
                  class="h-5 w-5"
                  :class="{ 'animate-spin': isUpdatingGeoDB }"
                />
                {{
                  isUpdatingGeoDB ? "Updating..." : "Update GeoLite Database"
                }}
              </button>

              <div
                v-if="updateMessage"
                class="text-sm font-semibold"
                :class="
                  updateMessage.includes('success')
                    ? 'text-secondary-400'
                    : 'text-rose-400'
                "
              >
                {{ updateMessage }}
              </div>
            </div>
          </div>
        </SectionCard>
      </div>
    </div>
    <div v-else class="relative flex h-64 items-center justify-center">
      <LoaderOverlay />
    </div>
  </div>
</template>

<style scoped>
@reference "src/style.css";
section {
  @apply backdrop-blur-sm;
}
</style>