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

<script setup lang="ts">
import DetailCard from "components/DetailView/DetailCard.vue";
import LoaderOverlay from "components/LoaderOverlay.vue";
import ScoreTag from "components/ScoreTag.vue";
import SectionCard from "components/SectionCard.vue";
import SparkChart from "components/SparkChart.vue";
import StatBarTable, { type StatBarItem } from "components/StatBarTable.vue";
import { countries } from "src/countries";
import { getHoneypot, useBlocklistStore, useDashboardStore } from "src/store";
import { splitSubnet, timeAgo } from "utils/formatting";
import { icons } from "utils/icons";
import { computed, onMounted } from "vue";

const dashboardStore = useDashboardStore();
const blocklistStore = useBlocklistStore();

const stats = computed(() => dashboardStore.stats);
const statsLoading = computed(() => dashboardStore.loading && !stats.value);
const blocklistLoading = computed(
  () => blocklistStore.loading && blocklistStore.blocklist.length === 0,
);

const blocklistData = computed(() => {
  return blocklistStore.blocklist.map((entry) => ({
    label: entry.address,
    expires: entry.expires,
    tags: entry.reason.split(","),
    routerLink: `/ip/${entry.address}`,
  }));
});

const topRemoteAddrsData = computed<StatBarItem[]>(() => {
  return (stats.value?.stats_24h?.remote_addrs || []).map((a) => ({
    label: a.label,
    count: a.count,
    routerLink: `/ip/${a.label}`,
  }));
});

const topPortsData = computed<StatBarItem[]>(() => {
  return (stats.value?.stats_24h?.ports || []).map((p) => ({
    label: p.label,
    count: p.count,
    routerLink: `/port/${p.label}`,
  }));
});

const topTypesData = computed<StatBarItem[]>(() => {
  return (stats.value?.stats_24h?.types || []).map((t) => ({
    label: getHoneypot(t.label)?.label ?? t.label,
    count: t.count,
    routerLink: `/honeypot/${t.label}`,
  }));
});

const topDomainsData = computed<StatBarItem[]>(() => {
  return (stats.value?.stats_24h?.domains || []).map((d) => ({
    label: d.label,
    count: d.count,
    routerLink: `/domain/${d.label}`,
  }));
});

const topCountriesData = computed<StatBarItem[]>(() => {
  return (stats.value?.stats_24h?.countries || []).map((c) => ({
    label:
      countries.get(c.label) !== undefined
        ? countries.get(c.label)?.[0] + " " + countries.get(c.label)?.[1]
        : c.label,
    count: c.count,
    routerLink: `/country/${c.label}`,
  }));
});

const sparkData = computed(() => {
  if (!stats.value?.sparkline) return { x: [], y: [] };
  return {
    x: stats.value.sparkline.map((s) => new Date(s.time).getTime()),
    y: stats.value.sparkline.map((s) => s.count),
  };
});

onMounted(() => {
  dashboardStore.fetchStats();
  blocklistStore.fetchBlocklist();
});
</script>

<template>
  <div class="page-container">
    <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
      <DetailCard label="24h Activity Trend" :icon="icons.activity">
        <div class="relative h-32 pt-2">
          <LoaderOverlay v-if="statsLoading" />
          <SparkChart
            :data="sparkData"
            class="text-primary-500 h-full w-full"
          />
        </div>
      </DetailCard>
      <DetailCard
        label="Last 24 Hours"
        :value="stats?.count_24h"
        :icon="icons.time"
        value-class="text-3xl font-bold"
      />
      <DetailCard
        label="Total Events"
        :value="
          stats?.stats_all?.types?.reduce((acc, curr) => acc + curr.count, 0) ??
          0
        "
        :icon="icons.total"
        value-class="text-3xl font-bold"
      />
    </div>

    <div class="grid grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3">
      <SectionCard
        :title="`Blocked Addresses (${blocklistData.length})`"
        :icon="icons.blocked"
        class="relative"
      >
        <LoaderOverlay v-if="blocklistLoading" />
        <div
          class="h-full max-h-[300px] overflow-y-auto"
          v-if="blocklistData.length > 0"
        >
          <table class="w-full text-sm">
            <thead>
              <tr class="text-stone-400">
                <th class="w-min pb-2 text-left">Address</th>
                <th class="w-full px-2 pb-2 text-left">Tags</th>
                <th class="w-min pb-2 text-right whitespace-nowrap">Expires</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="s in blocklistData" :key="s.label">
                <td class="w-min">
                  <router-link
                    :to="s.routerLink"
                    class="hover:text-secondary-400"
                  >
                    <span>
                      {{ splitSubnet(s.label).address
                      }}<span class="text-secondary-300">{{
                        splitSubnet(s.label).mask
                      }}</span>
                    </span>
                  </router-link>
                </td>
                <td class="w-full px-2">
                  <span class="inline-flex flex-wrap gap-0.5">
                    <ScoreTag v-for="tag in s.tags" :key="tag" :tag="tag" />
                  </span>
                </td>
                <td class="w-min text-right whitespace-nowrap">
                  {{ timeAgo(s.expires) }}
                </td>
              </tr>
            </tbody>
          </table>
        </div>
        <div
          v-else
          class="flex items-center justify-center py-4 text-sm text-stone-500"
        >
          No data available
        </div>
      </SectionCard>

      <SectionCard title="Top Remote Addresses" :icon="icons.address">
        <StatBarTable
          :items="topRemoteAddrsData"
          :loading="statsLoading"
          class="max-h-[300px]"
        />
      </SectionCard>

      <SectionCard title="Top Countries" :icon="icons.country">
        <StatBarTable
          :items="topCountriesData"
          :loading="statsLoading"
          class="max-h-[300px]"
        />
      </SectionCard>

      <SectionCard title="Top Domains" :icon="icons.domain">
        <StatBarTable
          :items="topDomainsData"
          :loading="statsLoading"
          class="max-h-[300px]"
        />
      </SectionCard>

      <SectionCard title="Top Ports" :icon="icons.port">
        <StatBarTable
          :items="topPortsData"
          :loading="statsLoading"
          class="max-h-[300px]"
        />
      </SectionCard>

      <SectionCard title="Top Honeypots" :icon="icons.honeypotType">
        <StatBarTable
          :items="topTypesData"
          :loading="statsLoading"
          class="max-h-[300px]"
        />
      </SectionCard>
    </div>
  </div>
</template>