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

<script setup lang="ts">
import type { Icon } from "@tabler/icons-vue";
import { useWindowSize } from "@vueuse/core";
import DetailCard from "components/DetailView/DetailCard.vue";
import DetailChart from "components/DetailView/DetailChart.vue";
import EventTable from "components/EventTable/EventTable.vue";
import EventTableMobileRow from "components/EventTable/EventTableMobileRow.vue";
import EventTableRow from "components/EventTable/EventTableRow.vue";
import LiveUpdateButton from "components/LiveUpdateButton.vue";
import PageHeader from "components/PageHeader.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 { useChartData } from "composables/useChartData";
import { useLogEntries } from "composables/useLogEntries";
import { countries } from "src/countries";
import {
  createFilterState,
  getHoneypot,
  useBlocklistStore,
  useFilterActions,
  useIPInfoStore,
} from "src/store";
import { type BlocklistEntry, type TopNField } from "src/types";
import { getSparkData } from "utils/filters";
import {
  formatLocalDateTime,
  formatLocalNumber,
  maskIp,
  timeAgo,
} from "utils/formatting";
import { icons } from "utils/icons";
import { isValidIP, isValidPort, URL } from "utils/utils";
import { computed, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";

const route = useRoute();
const router = useRouter();

// -- View Type Determination --
const viewType = computed(() => {
  const name = route.name as string;
  if (name === "ip") return "ip";
  if (name === "subnet") return "subnet";
  if (name === "port") return "port";
  if (name === "honeypot") return "honeypot";
  if (["city", "country", "asn", "domain", "fqdn"].includes(name)) return "geo";
  return "unknown";
});

// -- Params & Identifiers --
const ip = computed(() => route.params.ip as string);
const port = computed(() => route.params.port as string);
const mask = computed(() =>
  route.params.mask ? parseInt(route.params.mask as string) : 24,
);
const honeypotParam = computed(() => route.params.honeypot as string);

const props = defineProps<{
  type?: string;
  value?: string;
}>();

const resolvedGeoType = computed(() => {
  if (viewType.value !== "geo") return "";
  if (props.type) return props.type;
  if (route.params.type) return route.params.type as string;
  const name = route.name as string;
  if (name === "domain") return "domain";
  if (name === "fqdn") return "fqdn";
  return "";
});

const resolvedGeoValue = computed(() => {
  if (viewType.value !== "geo") return "";
  if (props.value) return props.value;
  if (route.params.value) return route.params.value as string;
  return "";
});

// -- Entity Identification --
// Used for display and filtering
const entityIdentifier = computed(() => {
  switch (viewType.value) {
    case "ip":
      return ip.value;
    case "subnet":
      return `${maskIp(ip.value, mask.value)}/${mask.value}`;
    case "port":
      return port.value;
    case "honeypot":
      return honeypotParam.value;
    case "geo":
      return resolvedGeoValue.value;
    default:
      return "";
  }
});

// -- Stores --
const blocklistStore = useBlocklistStore();
const ipInfoStore = useIPInfoStore();
const { getIPInfo, ipInfoIsLoading, getDomainByIp } = ipInfoStore;

// -- IP Specific Computed --
const ipDomain = computed(() =>
  viewType.value === "ip" ? getDomainByIp(ip.value).value : null,
);
const ipGeo = computed(() =>
  viewType.value === "ip" || viewType.value === "subnet"
    ? getIPInfo(ip.value).value
    : null,
);

// -- Blocklist / Tags --
const tags = computed(() => {
  if (viewType.value === "ip" || viewType.value === "subnet") {
    return blocklistStore.getTagsByIp(entityIdentifier.value);
  }
  return [];
});

const expiration = computed(() => {
  if (viewType.value === "ip" || viewType.value === "subnet") {
    const entry = blocklistStore.getBlocklistEntryByIp(entityIdentifier.value);
    if (entry) return entry.expires;
    const botnet = blocklistStore.getBotnetEntryByIp(entityIdentifier.value);
    if (botnet) return botnet.expires;
  }
  return null;
});

// -- Filter Setup --
const filterInitialState = computed<Partial<import("src/store").FilterState>>(
  () => {
    switch (viewType.value) {
      case "ip":
        return { remote_addr: [ip.value] };
      case "subnet":
        return { remote_addr: [entityIdentifier.value] }; // Subnet string
      case "port":
        return { dst_port: [port.value] };
      case "honeypot":
        // Honeypot filter uses 'type' which corresponds to honeypot name
        const hp = getHoneypot(honeypotParam.value);
        return { type: hp ? [hp.name] : [] };
      case "geo":
        if (
          resolvedGeoType.value &&
          ["asn", "city", "country", "domain", "fqdn"].includes(
            resolvedGeoType.value,
          )
        ) {
          return { [resolvedGeoType.value]: [resolvedGeoValue.value] };
        }
        return {};
      default:
        return {};
    }
  },
);

const localFilterActions = useFilterActions(
  createFilterState(filterInitialState.value),
);

// Watch for route changes to update filter
watch(
  [viewType, entityIdentifier, resolvedGeoType, resolvedGeoValue],
  () => {
    // Reset relevant fields
    localFilterActions.state.remote_addr = [];
    localFilterActions.state.dst_port = [];
    localFilterActions.state.country = [];
    localFilterActions.state.city = [];
    localFilterActions.state.asn = [];
    localFilterActions.state.domain = [];
    localFilterActions.state.fqdn = [];
    localFilterActions.state.type = [];

    if (viewType.value === "ip") {
      localFilterActions.state.remote_addr = [ip.value];
    } else if (viewType.value === "subnet") {
      localFilterActions.state.remote_addr = [entityIdentifier.value];
    } else if (viewType.value === "port") {
      localFilterActions.state.dst_port = [port.value];
    } else if (viewType.value === "honeypot") {
      const hp = getHoneypot(honeypotParam.value);
      if (hp) localFilterActions.state.type = [hp.name];
    } else if (viewType.value === "geo") {
      if (
        resolvedGeoType.value &&
        ["asn", "city", "country", "domain", "fqdn"].includes(
          resolvedGeoType.value,
        )
      ) {
        (localFilterActions.state as any)[resolvedGeoType.value] = [
          resolvedGeoValue.value,
        ];
      }
    }
  },
  { deep: true },
);

// -- Data States --

// Common Stats Interface
interface GeneralStats {
  total_events: number;
  first_seen?: string;
  last_seen?: string;
  top_ports?: TopNField[];
  top_events?: TopNField[];
  top_addrs?: TopNField[]; // For Port/Geo/Honeypot
  top_subdomains?: TopNField[]; // For Geo
  blocklist?: BlocklistEntry[]; // For IP/Subnet
  title?: string; // Geo
  metadata?: Record<string, any>; // Geo
  remote_addrs?: TopNField[];
}

const stats = ref<GeneralStats | null>(null);
const loadingStats = ref(false);

// Subnet Stats (Specific to IP View for the subnet card)
const subnetMaskForIPView = ref(24);
const ipViewSubnetStats = ref<TopNField[]>([]);
const loadingIPViewSubnet = ref(false);

// Subnet View Specific
// The active addresses in the subnet
const subnetViewActiveAddrs = ref<TopNField[]>([]);
const loadingSubnetViewActiveAddrs = ref(false);

// Honeypot other stats
const rawHoneypotStats = ref<Record<string, any>>({});

// -- Fetching Logic --

async function fetchStats() {
  loadingStats.value = true;
  stats.value = null;
  rawHoneypotStats.value = {};
  let url = "";

  try {
    if (viewType.value === "ip") {
      if (!ip.value) return;
      url = `${URL()}/api/stats/ip?ip=${ip.value}`;
    } else if (viewType.value === "subnet") {
      if (!ip.value) return; // entityIdentifier has mask
      url = `${URL()}/api/stats/ip?ip=${entityIdentifier.value}`; // API uses 'ip' param for subnet too? Checked SubnetDetailView: yes, ?ip=${subnet.value}
    } else if (viewType.value === "port") {
      if (!port.value) return;
      url = `${URL()}/api/stats/port?port=${port.value}`;
    } else if (viewType.value === "honeypot") {
      const hp = getHoneypot(honeypotParam.value);
      if (!hp) return;
      url = `${URL()}/api/stats/honeypot?event_type=${hp.name}`;
    } else if (viewType.value === "geo") {
      if (!resolvedGeoValue.value) return;
      url = `${URL()}/api/stats/geo?type=${resolvedGeoType.value}&value=${resolvedGeoValue.value}`;
    }

    if (url) {
      const res = await fetch(url);
      if (res.ok) {
        const data = await res.json();

        if (viewType.value === "honeypot") {
          rawHoneypotStats.value = data;
        }
        stats.value = data;
      }
    }
  } catch (e) {
    console.error("Failed to fetch stats", e);
  } finally {
    loadingStats.value = false;
  }
}

// IP View: Fetch Subnet Preview
async function fetchIPViewSubnetStats() {
  if (viewType.value !== "ip" || !ip.value) return;
  loadingIPViewSubnet.value = true;
  try {
    const res = await fetch(
      `${URL()}/api/stats/subnet?ip=${ip.value}&mask=${subnetMaskForIPView.value}`,
    );
    if (res.ok) {
      ipViewSubnetStats.value = await res.json();
    }
  } catch (e) {
    console.error("Failed to fetch subnet stats for IP view", e);
  } finally {
    loadingIPViewSubnet.value = false;
  }
}

// Subnet View: Fetch Active Addresses (Wait, SubnetDetailView calls `api/stats/subnet` for this)
async function fetchSubnetViewStats() {
  if (viewType.value !== "subnet" || !ip.value) return;
  loadingSubnetViewActiveAddrs.value = true;
  try {
    const res = await fetch(
      `${URL()}/api/stats/subnet?ip=${ip.value}&mask=${mask.value}`,
    );
    if (res.ok) {
      subnetViewActiveAddrs.value = await res.json();
    }
  } catch (e) {
    console.error("Failed to fetch subnet stats", e);
  } finally {
    loadingSubnetViewActiveAddrs.value = false;
  }
}

// -- Watchers for Data Fetching --
watch(
  () => [route.path, viewType.value],
  () => {
    // Basic validation
    if (viewType.value === "ip" && !isValidIP(ip.value)) {
      router.push("/");
      return;
    }
    if (viewType.value === "port" && !isValidPort(port.value)) {
      router.push("/");
      return;
    }
    if (
      viewType.value === "subnet" &&
      (!isValidIP(ip.value) || isNaN(mask.value))
    ) {
      router.push("/");
      return;
    }
    if (viewType.value === "honeypot" && !honeypotParam.value) {
      router.push("/");
      return;
    }

    fetchStats();
    if (viewType.value === "ip") fetchIPViewSubnetStats();
    if (viewType.value === "subnet") fetchSubnetViewStats();
  },
  { immediate: true },
);

watch(subnetMaskForIPView, () => {
  if (viewType.value === "ip") fetchIPViewSubnetStats();
});

// -- Charts & Logs --

const chartFilterState = computed(() =>
  createFilterState({
    ...filterInitialState.value,
    limit: 10000,
    columns: ["time", "event", "dst_port", "remote_addr", "type"],
  }),
);

const { chartData: loadedChartData, loading: loadingAll } = useChartData({
  filters: () => chartFilterState.value,
});

const {
  entries: tableEntries,
  total: tableTotal,
  loading: loadingTable,
  connectionStatus: tableStatus,
  connect: connectTable,
  disconnect: disconnectTable,
} = useLogEntries({
  filterState: localFilterActions.state,
});

const sparkData = computed(() => getSparkData(loadedChartData.value));
const chartSelectedCount = ref(0);
const chartSelectedIPs = ref(0);

// -- Connection Toggle --
function toggleConnection() {
  if (tableStatus.value === "open") {
    disconnectTable();
  } else {
    connectTable();
  }
}

// -- Derived Data for UI --
const topPortsData = computed<StatBarItem[]>(() => {
  return (stats.value?.top_ports || []).map((p) => ({
    label: p.label,
    count: p.count,
    routerLink: `/port/${p.label}`,
  }));
});

const topEventsData = computed<StatBarItem[]>(() => {
  return (stats.value?.top_events || []).map((e) => ({
    id: e.label,
    label:
      viewType.value === "port"
        ? (getHoneypot(e.label)?.label ?? e.label)
        : e.label,
    count: e.count,
  }));
});

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

const topSubdomainsData = computed<StatBarItem[]>(() => {
  return (stats.value?.top_subdomains || []).map((f) => ({
    label: f.label,
    count: f.count,
    routerLink: `/fqdn/${f.label}`,
  }));
});

// For IP view subnet card
const ipViewSubnetStatsData = computed<StatBarItem[]>(() => {
  return ipViewSubnetStats.value.map((s) => ({
    label: s.label,
    count: s.count,
    routerLink: `/ip/${s.label}`,
  }));
});

// For Subnet view active addresses
const subnetViewStatsData = computed<StatBarItem[]>(() => {
  return subnetViewActiveAddrs.value.map((s) => ({
    label: s.label,
    count: s.count,
    routerLink: `/ip/${s.label}`,
  }));
});

const otherTopStats = computed(() => {
  if (viewType.value !== "honeypot") return [];
  const standardKeys = [
    "top_events",
    "top_addrs",
    "top_ports",
    "total_events",
    "first_seen",
    "last_seen",
    "title",
    "metadata",
  ];
  return Object.keys(rawHoneypotStats.value).filter(
    (key) => !standardKeys.includes(key),
  );
});

// Geo Toggle
const showSubdomains = ref(false);

// Titles & Icons
const pageTitle = computed<{ title: string; icon: Icon | undefined }>(() => {
  if (viewType.value === "ip")
    return { title: `${ip.value}`, icon: icons.address };

  if (viewType.value === "subnet")
    return { title: `${entityIdentifier.value}`, icon: icons.subnet };

  if (viewType.value === "port")
    return { title: `Port ${port.value}`, icon: icons.port };

  if (viewType.value === "honeypot") {
    const hp = getHoneypot(honeypotParam.value);
    return {
      title: hp ? `${hp.label} Honeypot` : honeypotParam.value,
      icon: icons.honeypotType,
    };
  }

  if (viewType.value === "geo") {
    if (loadingStats.value) return { title: "Loading...", icon: undefined };
    if (resolvedGeoType.value === "country") {
      const code = stats.value?.title || resolvedGeoValue.value;
      const title =
        (countries.get(code)?.[1] || code) +
        " " +
        (countries.get(code)?.[0] || "");
      return { title, icon: icons.country };
    }
    if (resolvedGeoType.value === "asn") {
      const org = stats.value?.metadata?.asn_org;
      return { title: org || `ASN ${resolvedGeoValue.value}`, icon: icons.asn };
    }

    if (
      resolvedGeoType.value === "domain" ||
      resolvedGeoType.value === "fqdn"
    ) {
      return {
        title: stats.value?.title || resolvedGeoValue.value,
        icon: icons.domain,
      };
    }

    return {
      title: stats.value?.title || resolvedGeoValue.value,
      icon: icons.city,
    };
  }
  return { title: "Details", icon: icons.activity };
});

// Geo Readable Name logic
const locationReadableName = computed(() => {
  if (loadingStats.value || !stats.value?.metadata) return null;
  const { country } = stats.value.metadata;

  if (resolvedGeoType.value === "city" && country) {
    return countries.get(country)?.[0] + " " + countries.get(country)?.[1];
  }
  if (resolvedGeoType.value === "asn") return `ASN ${resolvedGeoValue.value}`;
  if (country && resolvedGeoType.value !== "country")
    return countries.get(country)?.[1];
  return null;
});

const chartLink = computed(() => {
  if (viewType.value === "port") return undefined;

  const state = filterInitialState.value;
  const keys = Object.keys(state);
  if (keys.length > 0) {
    const key = keys[0];
    if (key) {
      const val = (state as any)[key];
      if (Array.isArray(val) && val.length > 0) {
        return `/charts?${key}=${val[0]}`;
      }
    }
  }
  return "/charts";
});

const eventTableColumns = computed(() => {
  if (viewType.value === "ip") return ["time", "event", "details"];
  return ["time", "event", "remote_addr", "details"];
});

const { width } = useWindowSize();
</script>

<template>
  <div class="page-container">
    <PageHeader
      :title="pageTitle.title"
      :icon="pageTitle.icon"
      class="min-h-12"
    >
      <template #left>
        <!-- IP/Subnet: Geo Info -->
        <div
          v-if="viewType === 'ip' || viewType === 'subnet'"
          class="flex flex-wrap items-center gap-8"
        >
          <div class="flex flex-col">
            <!-- Domain resolution for IP -->
            <p
              v-if="viewType === 'ip' && ipDomain && ipDomain !== ip"
              class="text-stone-400"
            >
              <template
                v-if="
                  ipGeo?.domain &&
                  ipGeo.domain !== ipDomain &&
                  ipDomain.endsWith(ipGeo.domain)
                "
              >
                <router-link
                  :to="`/fqdn/${ipDomain}`"
                  class="peer hover:text-stone-50"
                >
                  {{ ipDomain.slice(0, -ipGeo.domain.length) }}
                </router-link>
                <router-link
                  :to="`/domain/${ipGeo.domain}`"
                  class="peer-hover:text-stone-50 hover:text-stone-50"
                >
                  {{ ipGeo.domain }}
                </router-link>
              </template>
              <template v-else>
                <router-link
                  :to="`/fqdn/${ipDomain}`"
                  class="hover:text-stone-50"
                >
                  {{ ipDomain }}
                </router-link>
              </template>
            </p>
            <p
              v-else-if="viewType === 'ip' && ipInfoIsLoading(ip)"
              class="text-muted animate-pulse"
            >
              Looking up domain...
            </p>

            <!-- Geo Info -->
            <p class="" v-if="ipGeo">
              <span
                v-if="ipGeo?.country?.iso_code"
                :title="countries.get(ipGeo.country.iso_code)?.[1]"
                class="stat-label"
              >
                {{ countries.get(ipGeo.country.iso_code)?.[0] }}
                <router-link
                  v-if="ipGeo?.city?.name"
                  :to="`/city/${ipGeo.city.name}`"
                  class="hover:text-stone-200"
                >
                  {{ ipGeo.city.name }},
                </router-link>
                <router-link
                  :to="`/country/${ipGeo.country.iso_code}`"
                  class="hover:text-stone-200"
                >
                  {{ countries.get(ipGeo.country.iso_code)?.[1] }}
                </router-link>
              </span>
              <span
                v-if="ipGeo?.asn && ipGeo?.country?.iso_code"
                class="text-muted"
                >,
              </span>
              <span
                v-if="ipGeo?.asn?.autonomous_system_organization"
                class="stat-label"
              >
                <router-link
                  :to="`/asn/${ipGeo.asn.autonomous_system_number}`"
                  class="hover:text-stone-200"
                >
                  {{ ipGeo?.asn?.autonomous_system_organization }} (ASN{{
                    ipGeo?.asn?.autonomous_system_number
                  }})
                </router-link>
              </span>
            </p>
          </div>
          <!-- Tags -->
          <div class="flex gap-2" v-if="tags && tags.length > 0">
            <ScoreTag :tag="tag" v-for="tag in tags" :key="tag" />
            <span class="stat-value" v-if="expiration">
              Unblocked {{ expiration ? timeAgo(expiration) : "N/A" }}
            </span>
          </div>
        </div>

        <!-- Geo View Header Extra -->
        <div
          v-else-if="viewType === 'geo' && stats?.metadata"
          class="flex items-center gap-4"
        >
          <div class="flex items-center gap-2 text-stone-300">
            <span v-if="locationReadableName" class="text-stone-400">
              {{ locationReadableName }}
            </span>
          </div>
        </div>
      </template>

      <template #actions>
        <LiveUpdateButton
          :st="tableStatus"
          @toggleConnection="toggleConnection"
        />
      </template>
    </PageHeader>

    <div
      class="grid grid-cols-1 gap-4 md:grid-cols-2 md:grid-rows-2 xl:grid-rows-1"
      :class="{
        'xl:grid-cols-[1fr_280px_250px_300px]':
          viewType === 'ip' || viewType === 'subnet',
        'lg:grid-cols-[1fr_350px]': viewType === 'port',
        'xl:grid-cols-[1fr_250px_350px]':
          viewType === 'geo' || viewType === 'honeypot',
      }"
    >
      <DetailChart
        v-model:selected-points="chartSelectedCount"
        v-model:selected-i-ps="chartSelectedIPs"
        :chartData="loadedChartData"
        :local-filter-actions="localFilterActions"
        title="Activity Timeline"
        :loading="loadingAll"
        :key="entityIdentifier"
        :chart-link="chartLink"
        :is-port="viewType === 'port'"
        :is-honeypot="viewType === 'honeypot'"
      />

      <!-- IP/Subnet: Blocklist -->
      <SectionCard
        v-if="viewType === 'ip' || viewType === 'subnet'"
        :title="`Blocklist History (${stats?.blocklist?.length || 0})`"
        :icon="icons.blocked"
      >
        <div class="h-75 overflow-y-auto pr-2 text-xs">
          <div v-if="loadingStats" class="animate-pulse py-4 text-center">
            Loading...
          </div>
          <div
            v-else-if="!stats?.blocklist?.length"
            class="py-4 text-center text-stone-500"
          >
            No blocklist history
          </div>
          <div
            v-for="entry in stats?.blocklist || []"
            :key="entry.id"
            class="my-1 flex items-center justify-between gap-2"
          >
            <div class="w-1/3 whitespace-nowrap">
              <template v-if="viewType === 'subnet'">
                {{ entry.address }}
                <br />
              </template>
              {{ formatLocalDateTime(entry.timestamp, true) }}
            </div>
            <div class="flex w-min flex-wrap justify-end gap-1">
              <ScoreTag
                v-for="tag in entry.reason.split(',')"
                :key="tag"
                :tag="tag.trim()"
              />
            </div>
          </div>
        </div>
      </SectionCard>

      <SectionCard
        v-if="viewType !== 'port'"
        title="Top Ports"
        :icon="icons.port"
      >
        <StatBarTable
          :items="topPortsData"
          :loading="loadingStats"
          bar-min-width="10px"
          class="max-h-75"
        />
      </SectionCard>

      <!-- IP: Subnet Preview -->
      <SectionCard
        v-if="viewType === 'ip'"
        :title="`Subnet (${ipViewSubnetStatsData.length})`"
        :icon="icons.subnet"
      >
        <template #header-right>
          <div class="flex items-center gap-2">
            <div class="flex gap-1">
              <select
                v-model="subnetMaskForIPView"
                class="rounded border border-stone-700 bg-stone-800 px-2 py-0.5 text-xs text-stone-400 transition-colors hover:bg-stone-700"
              >
                <option
                  v-for="m in [24, 22, 20, 18, 16, 12, 8]"
                  :key="m"
                  :value="m"
                >
                  /{{ m }}
                </option>
              </select>
            </div>
            <router-link
              :to="`/ip/${ip}/${subnetMaskForIPView}`"
              class="btn-secondary p-0.5 text-[10px]"
              title="View full subnet details"
            >
              <component :is="icons.subnet" />
            </router-link>
          </div>
        </template>
        <StatBarTable
          :items="ipViewSubnetStatsData"
          :loading="loadingIPViewSubnet"
          bar-min-width="10px"
          class="max-h-75"
        />
      </SectionCard>

      <!-- Subnet: Active Addresses -->
      <SectionCard
        v-if="viewType === 'subnet'"
        :title="`Active Addresses (${subnetViewStatsData.length})`"
        :icon="icons.address"
      >
        <StatBarTable
          :items="subnetViewStatsData"
          :loading="loadingSubnetViewActiveAddrs"
          bar-min-width="10px"
          class="max-h-[300px]"
        />
      </SectionCard>

      <!-- Geo: Top Domains/Addresses Switch -->
      <SectionCard
        v-if="viewType === 'geo'"
        :title="showSubdomains ? 'Top Domains' : 'Top Addresses'"
        :icon="showSubdomains ? icons.domain : icons.address"
      >
        <div class="mb-2 flex">
          <div class="flex rounded bg-stone-800 p-0.5">
            <button
              @click="showSubdomains = false"
              class="rounded px-2 py-0.5 text-xs font-medium transition-colors"
              :class="
                !showSubdomains
                  ? 'bg-stone-700 text-stone-200 shadow-sm'
                  : 'text-stone-400 hover:text-stone-200'
              "
            >
              <component :is="icons.address" size="20" /> IP
            </button>
            <button
              @click="showSubdomains = true"
              class="rounded px-2 py-0.5 text-xs font-medium transition-colors"
              :class="
                showSubdomains
                  ? 'bg-stone-700 text-stone-200 shadow-sm'
                  : 'text-stone-400 hover:text-stone-200'
              "
            >
              <component :is="icons.domain" size="20" /> Domain
            </button>
          </div>
        </div>
        <StatBarTable
          :items="showSubdomains ? topSubdomainsData : topAddrsData"
          :loading="loadingStats"
          bar-min-width="10px"
          class="max-h-75"
        />
      </SectionCard>

      <!-- Port/Honeypot: Top Addresses (No switch) -->
      <SectionCard
        v-if="viewType === 'port' || viewType === 'honeypot'"
        title="Top Addresses"
        :icon="icons.address"
      >
        <StatBarTable
          :items="topAddrsData"
          :loading="loadingStats"
          bar-min-width="10px"
          class="max-h-[400px]"
        />
      </SectionCard>
    </div>

    <!-- Stats Grid (Common) -->
    <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
      <DetailCard label="Event Types" :icon="icons.eventType">
        <div class="relative flex flex-col pt-2">
          <StatBarTable :items="topEventsData" :loading="loadingAll" />
        </div>
      </DetailCard>
      <DetailCard label="Recent Activity" :icon="icons.activity">
        <SparkChart :data="sparkData" class="h-32" />
      </DetailCard>
      <DetailCard
        label="Total Events"
        :value="stats?.total_events"
        :icon="icons.total"
      />
      <DetailCard
        v-if="viewType !== 'honeypot'"
        label="Dates"
        :icon="icons.time"
      >
        <div class="stat-label mt-4">First Seen:</div>
        <div class="stat-value">
          {{ timeAgo(stats?.first_seen) }}
          <span class="text-muted font-normal">
            ({{
              stats?.first_seen ? formatLocalDateTime(stats.first_seen) : ""
            }})
          </span>
        </div>
        <div class="stat-label mt-4">Last Seen:</div>
        <div class="stat-value">
          {{ timeAgo(stats?.last_seen) }}
          <span class="text-muted font-normal">
            ({{ stats?.last_seen ? formatLocalDateTime(stats.last_seen) : "" }})
          </span>
        </div>
      </DetailCard>
      <!-- Honeypot: Top Ports -->
      <DetailCard
        v-if="viewType === 'honeypot'"
        label="Destination Ports"
        :value="topPortsData.length"
        :icon="icons.port"
      >
        <StatBarTable
          :items="topPortsData"
          :loading="loadingAll"
          bar-min-width="10px"
          class="max-h-[100px]"
        />
      </DetailCard>
    </div>

    <!-- Honeypot: Extra Stats -->
    <details v-if="otherTopStats.length > 0">
      <summary class="text-sm text-stone-400">Show more stats</summary>
      <div class="mt-4 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
        <DetailCard v-for="stat in otherTopStats" :key="stat" :label="stat">
          <StatBarTable
            :items="rawHoneypotStats[stat] || []"
            :loading="loadingStats"
            class="max-h-[300px]"
            label-column-width="w-[min-content] max-w-48 truncate"
            :add-title="true"
          />
        </DetailCard>
      </div>
    </details>

    <!-- Table (Common) -->
    <EventTable
      :events="tableEntries"
      :total-events="tableTotal"
      :loading="loadingTable"
      :columns="eventTableColumns"
      :filter-actions="localFilterActions"
    >
      <template #header-right>
        <div class="stat-label">
          {{ formatLocalNumber(stats?.total_events || 0) }} total events
          <template v-if="chartSelectedCount > 0">
            ({{ formatLocalNumber(chartSelectedCount) }} events,
            {{ formatLocalNumber(chartSelectedIPs) }} IPs selected)
          </template>
        </div>
      </template>
      <template #rows="{ events }">
        <EventTableRow
          v-if="width > 768"
          v-for="ev in events"
          :key="ev.id"
          :evt="ev"
          :columns="eventTableColumns"
          :filter-actions="localFilterActions"
          :width="width"
        />
        <EventTableMobileRow
          v-else
          v-for="ev in events"
          :key="`mobile-${ev.id}`"
          :evt="ev"
          :columns="eventTableColumns"
          :filter-actions="localFilterActions"
          :width="width"
        />
      </template>
    </EventTable>
  </div>
</template>