<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>