mirror of
https://github.com/rajnandan1/kener.git
synced 2026-06-23 04:10:22 +00:00
452 lines
16 KiB
Svelte
452 lines
16 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import { format } from "date-fns";
|
|
import { resolve } from "$app/paths";
|
|
import TrendingUp from "lucide-svelte/icons/trending-up";
|
|
import Clock from "lucide-svelte/icons/clock";
|
|
import Activity from "lucide-svelte/icons/activity";
|
|
import { Button } from "$lib/components/ui/button/index.js";
|
|
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
|
import { Badge } from "$lib/components/ui/badge/index.js";
|
|
import { Skeleton } from "$lib/components/ui/skeleton/index.js";
|
|
import LoaderBoxes from "$lib/components/loaderbox.svelte";
|
|
import constants from "$lib/global-constants.js";
|
|
import * as Tabs from "$lib/components/ui/tabs/index.js";
|
|
import { page } from "$app/state";
|
|
import { AreaChart, Area, LinearGradient } from "layerchart";
|
|
import { curveCatmullRom } from "d3-shape";
|
|
import { scaleOrdinal, scaleSequential, scaleTime } from "d3-scale";
|
|
import IncidentItem from "$lib/components/IncidentItem.svelte";
|
|
import MaintenanceItem from "$lib/components/MaintenanceItem.svelte";
|
|
|
|
import * as Chart from "$lib/components/ui/chart/index.js";
|
|
import type { IncidentForMonitorList, MaintenanceEventsMonitorList } from "$lib/server/types/db";
|
|
interface DayDetailData {
|
|
minutes: Array<{
|
|
timestamp: number;
|
|
status: string;
|
|
}>;
|
|
uptime: string;
|
|
}
|
|
|
|
interface DayLatencyData {
|
|
minutes: Array<{
|
|
timestamp: number;
|
|
latency: number;
|
|
}>;
|
|
avgLatency: string;
|
|
}
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
monitorTag: string;
|
|
selectedDay: {
|
|
timestamp: number;
|
|
status: string;
|
|
} | null;
|
|
}
|
|
|
|
let { open = $bindable(), monitorTag, selectedDay }: Props = $props();
|
|
|
|
let loading = $state(false);
|
|
let latencyLoading = $state(false);
|
|
let incidentsLoading = $state(false);
|
|
let maintenancesLoading = $state(false);
|
|
let activeView = $state<"status" | "latency" | "incidents" | "maintenances">("status");
|
|
let dayDetailData = $state<DayDetailData | null>(null);
|
|
let dayLatencyData = $state<DayLatencyData | null>(null);
|
|
let dayIncidentsData = $state<IncidentForMonitorList[]>([]);
|
|
let dayMaintenancesData = $state<MaintenanceEventsMonitorList[]>([]);
|
|
// Chart config for latency
|
|
const chartConfig = {
|
|
latency: {
|
|
label: "Latency",
|
|
color: "var(--chart-1)"
|
|
}
|
|
} satisfies Chart.ChartConfig;
|
|
|
|
// Transform latency data for chart
|
|
let chartData = $derived.by(() => {
|
|
if (!dayLatencyData?.minutes) return [];
|
|
return dayLatencyData.minutes
|
|
.filter((d) => d.latency > 0)
|
|
.map((d) => ({
|
|
date: new Date(d.timestamp * 1000),
|
|
latency: d.latency
|
|
}));
|
|
});
|
|
|
|
// Fetch day detail data
|
|
async function fetchDayDetail() {
|
|
if (!selectedDay) return;
|
|
|
|
loading = true;
|
|
dayDetailData = null;
|
|
try {
|
|
const response = await fetch(resolve("/dashboard-apis/monitor-day-status"), {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
tag: monitorTag,
|
|
dayTimestamp: selectedDay.timestamp,
|
|
startOfDayTodayAtTz: selectedDay.timestamp,
|
|
nowAtTz: Math.min(selectedDay.timestamp + 86400 - 60, page.data.nowAtTz)
|
|
})
|
|
});
|
|
if (response.ok) {
|
|
dayDetailData = await response.json();
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch day detail:", error);
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
// Fetch day latency data
|
|
async function fetchDayLatency() {
|
|
if (!selectedDay) return;
|
|
|
|
latencyLoading = true;
|
|
dayLatencyData = null;
|
|
try {
|
|
const response = await fetch(resolve("/dashboard-apis/monitor-day-latency"), {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
tag: monitorTag,
|
|
startOfDayTodayAtTz: selectedDay.timestamp,
|
|
nowAtTz: Math.min(selectedDay.timestamp + 86400 - 60, page.data.nowAtTz)
|
|
})
|
|
});
|
|
if (response.ok) {
|
|
dayLatencyData = await response.json();
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch day latency:", error);
|
|
} finally {
|
|
latencyLoading = false;
|
|
}
|
|
}
|
|
|
|
//// Fetch day incident data
|
|
async function fetchDayIncidents() {
|
|
if (!selectedDay) return;
|
|
|
|
incidentsLoading = true;
|
|
dayIncidentsData = [];
|
|
try {
|
|
const response = await fetch(resolve("/dashboard-apis/monitor-day-incidents"), {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
tag: monitorTag,
|
|
startOfDayTodayAtTz: selectedDay.timestamp,
|
|
nowAtTz: Math.min(selectedDay.timestamp + 86400 - 60, page.data.nowAtTz)
|
|
})
|
|
});
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
dayIncidentsData = result.incidents;
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch day incidents:", error);
|
|
} finally {
|
|
incidentsLoading = false;
|
|
}
|
|
}
|
|
|
|
// Fetch day maintenance data
|
|
async function fetchDayMaintenances() {
|
|
if (!selectedDay) return;
|
|
|
|
maintenancesLoading = true;
|
|
dayMaintenancesData = [];
|
|
try {
|
|
const response = await fetch(resolve("/dashboard-apis/monitor-day-maintenances"), {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
tag: monitorTag,
|
|
startOfDayTodayAtTz: selectedDay.timestamp,
|
|
nowAtTz: Math.min(selectedDay.timestamp + 86400 - 60, page.data.nowAtTz)
|
|
})
|
|
});
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
dayMaintenancesData = result.maintenances;
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch day maintenances:", error);
|
|
} finally {
|
|
maintenancesLoading = false;
|
|
}
|
|
}
|
|
// Format date for display
|
|
function formatDate(timestamp: number): string {
|
|
return format(new Date(timestamp * 1000), "EEEE, MMMM do, yyyy");
|
|
}
|
|
|
|
function formatTime(timestamp: number): string {
|
|
return format(new Date(timestamp * 1000), "HH:mm");
|
|
}
|
|
|
|
// Fetch data when dialog opens with a new day
|
|
$effect(() => {
|
|
if (open && selectedDay) {
|
|
fetchDayDetail();
|
|
fetchDayLatency();
|
|
fetchDayIncidents();
|
|
fetchDayMaintenances();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<Dialog.Root bind:open>
|
|
<Dialog.Overlay class="backdrop-blur-[2px]" />
|
|
<Dialog.Content class="max-h-[80vh] overflow-y-auto rounded-3xl sm:max-w-2xl">
|
|
<Dialog.Header>
|
|
<Dialog.Title class="flex items-center gap-2">
|
|
<Activity class="h-5 w-5" />
|
|
{selectedDay ? formatDate(selectedDay.timestamp) : ""}
|
|
</Dialog.Title>
|
|
<Dialog.Description>Minute-by-minute status data for this day</Dialog.Description>
|
|
</Dialog.Header>
|
|
|
|
<Tabs.Root value={activeView} class="bg-background w-full overflow-hidden rounded-3xl border">
|
|
<Tabs.List class="h-auto w-full justify-end rounded-none px-2 py-2">
|
|
<Tabs.Trigger value="status" class="rounded-3xl py-2">Status</Tabs.Trigger>
|
|
<Tabs.Trigger value="latency" class="rounded-3xl py-2">Latency</Tabs.Trigger>
|
|
<Tabs.Trigger value="incidents" class="rounded-3xl py-2">Incidents</Tabs.Trigger>
|
|
<Tabs.Trigger value="maintenances" class="rounded-3xl py-2">Maintenances</Tabs.Trigger>
|
|
</Tabs.List>
|
|
<!-- status view -->
|
|
<Tabs.Content value="status" class="p-4">
|
|
{#if loading}
|
|
<!-- Loading state -->
|
|
<div class="space-y-4 py-4">
|
|
<div class="flex items-center justify-between">
|
|
<Skeleton class="h-6 w-32" />
|
|
<Skeleton class="h-6 w-24" />
|
|
</div>
|
|
<div class="flex flex-wrap">
|
|
<LoaderBoxes />
|
|
</div>
|
|
</div>
|
|
{:else if dayDetailData}
|
|
<div class="space-y-4">
|
|
<!-- Minute grid -->
|
|
<div>
|
|
<div class="text-muted-foreground mb-2 flex items-center justify-between text-sm font-medium">
|
|
<p class="">Per-Minute Status</p>
|
|
<div class="flex items-center gap-1">
|
|
<TrendingUp class="h-3 w-3" />
|
|
{dayDetailData.uptime}%
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-0.5">
|
|
{#each dayDetailData.minutes as minute, index (minute.timestamp)}
|
|
<div class="group relative">
|
|
<div
|
|
class="h-2.5 w-2.5 cursor-pointer bg-{minute.status.toLowerCase()} rounded-[1px] transition-opacity hover:opacity-75"
|
|
title="{formatTime(minute.timestamp)} - {minute.status} - {index}"
|
|
></div>
|
|
|
|
<div
|
|
class="bg-popover text-popover-foreground pointer-events-none absolute bottom-full left-1/2 z-50 mb-1 hidden -translate-x-1/2 rounded border px-2 py-1 text-xs whitespace-nowrap shadow-md group-hover:block"
|
|
>
|
|
{formatTime(minute.timestamp)}
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
<div class="text-muted-foreground mt-2 flex justify-between text-xs">
|
|
<span>00:00</span>
|
|
<span>06:00</span>
|
|
<span>12:00</span>
|
|
<span>18:00</span>
|
|
<span>23:59</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<div class="py-8 text-center">
|
|
<p class="text-muted-foreground">Failed to load data</p>
|
|
</div>
|
|
{/if}
|
|
</Tabs.Content>
|
|
<Tabs.Content value="latency" class="p-4">
|
|
{#if latencyLoading}
|
|
<!-- Loading state -->
|
|
<div class="space-y-4 py-4">
|
|
<div class="flex items-center justify-between">
|
|
<Skeleton class="h-6 w-32" />
|
|
<Skeleton class="h-6 w-24" />
|
|
</div>
|
|
<Skeleton class="h-64 w-full" />
|
|
</div>
|
|
{:else if dayLatencyData && chartData.length > 0}
|
|
<div class="space-y-4">
|
|
<!-- Average latency -->
|
|
<div class="text-muted-foreground mb-2 flex items-center justify-between text-sm font-medium">
|
|
<p>Latency Over Time</p>
|
|
<div class="flex items-center gap-1">
|
|
<Clock class="h-3 w-3" />
|
|
{dayLatencyData.avgLatency} Avg
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Latency chart -->
|
|
<Chart.Container config={chartConfig} class="w-full" style="height: 256px;">
|
|
<AreaChart
|
|
data={chartData}
|
|
x="date"
|
|
xScale={scaleTime()}
|
|
y="latency"
|
|
yDomain={[0, null]}
|
|
yNice
|
|
axis="x"
|
|
grid={false}
|
|
series={[
|
|
{
|
|
key: "latency",
|
|
label: "Latency",
|
|
color: "var(--color-latency)"
|
|
}
|
|
]}
|
|
props={{
|
|
area: {
|
|
curve: curveCatmullRom,
|
|
"fill-opacity": 0.4,
|
|
line: { class: "stroke-1" }
|
|
},
|
|
xAxis: {
|
|
format: (d: Date) => format(d, "HH:mm")
|
|
}
|
|
}}
|
|
>
|
|
{#snippet marks({ series, getAreaProps })}
|
|
{#each series as s, i (s.key)}
|
|
<LinearGradient
|
|
stops={[s.color ?? "", "color-mix(in lch, " + s.color + " 10%, transparent)"]}
|
|
vertical
|
|
>
|
|
{#snippet children({ gradient })}
|
|
<Area {...getAreaProps(s, i)} fill={gradient} />
|
|
{/snippet}
|
|
</LinearGradient>
|
|
{/each}
|
|
{/snippet}
|
|
{#snippet tooltip()}
|
|
<Chart.Tooltip hideLabel>
|
|
{#snippet formatter({ value, name, item })}
|
|
<div class="flex w-full items-start gap-2">
|
|
<div
|
|
style="--color-bg: {item.color}; --color-border: {item.color};"
|
|
class="mt-0.5 size-2.5 shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)"
|
|
></div>
|
|
<div class="flex flex-1 flex-col items-start justify-between gap-1 leading-none">
|
|
<span class="text-muted-foreground text-xs"
|
|
>{item.payload?.date ? format(item.payload.date, "HH:mm") : ""}</span
|
|
>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-foreground font-mono font-medium tabular-nums">
|
|
{Math.round(Number(value))} ms
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/snippet}
|
|
</Chart.Tooltip>
|
|
{/snippet}
|
|
</AreaChart>
|
|
</Chart.Container>
|
|
</div>
|
|
{:else if dayLatencyData && chartData.length === 0}
|
|
<div class="py-8 text-center">
|
|
<p class="text-muted-foreground">No latency data available for this day</p>
|
|
</div>
|
|
{:else}
|
|
<div class="py-8 text-center">
|
|
<p class="text-muted-foreground">Failed to load latency data</p>
|
|
</div>
|
|
{/if}
|
|
</Tabs.Content>
|
|
<Tabs.Content value="incidents" class="p-4">
|
|
{#if incidentsLoading}
|
|
<!-- Loading state -->
|
|
<div class="space-y-4 py-4">
|
|
<div class="flex items-center justify-between">
|
|
<Skeleton class="h-6 w-32" />
|
|
<Skeleton class="h-6 w-24" />
|
|
</div>
|
|
<Skeleton class="h-64 w-full" />
|
|
</div>
|
|
{:else if dayIncidentsData.length > 0}
|
|
<div class="space-y-4">
|
|
<!-- Incident list -->
|
|
<div>
|
|
<div class="text-muted-foreground mb-2 flex items-center justify-between text-sm font-medium">
|
|
<p class="">Incidents</p>
|
|
<div class="flex items-center gap-1">
|
|
<Clock class="h-3 w-3" />
|
|
{dayIncidentsData.length} Total
|
|
</div>
|
|
</div>
|
|
|
|
<div class="scrollbar-hidden flex max-h-100 flex-col gap-4 overflow-y-auto">
|
|
{#each dayIncidentsData as incident (incident.id)}
|
|
<div class="border-b pb-5 last:border-b-0">
|
|
<IncidentItem {incident} hideMonitors={true} />
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<div class="py-8 text-center">
|
|
<p class="text-muted-foreground">No incidents for this day</p>
|
|
</div>
|
|
{/if}
|
|
</Tabs.Content>
|
|
<Tabs.Content value="maintenances" class="p-4">
|
|
{#if maintenancesLoading}
|
|
<!-- Loading state -->
|
|
<div class="space-y-4 py-4">
|
|
<div class="flex items-center justify-between">
|
|
<Skeleton class="h-6 w-32" />
|
|
<Skeleton class="h-6 w-24" />
|
|
</div>
|
|
<Skeleton class="h-64 w-full" />
|
|
</div>
|
|
{:else if dayMaintenancesData.length > 0}
|
|
<div class="space-y-4">
|
|
<!-- Maintenance list -->
|
|
<div>
|
|
<div class="text-muted-foreground mb-2 flex items-center justify-between text-sm font-medium">
|
|
<p class="">Maintenances</p>
|
|
<div class="flex items-center gap-1">
|
|
<Clock class="h-3 w-3" />
|
|
{dayMaintenancesData.length} Total
|
|
</div>
|
|
</div>
|
|
<div class="scrollbar-hidden flex max-h-100 flex-col gap-4 overflow-y-auto">
|
|
{#each dayMaintenancesData as maintenance (maintenance.id)}
|
|
<div class="border-b pb-5 last:border-b-0">
|
|
<MaintenanceItem {maintenance} hideMonitors={true} />
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<div class="py-8 text-center">
|
|
<p class="text-muted-foreground">No maintenances for this day</p>
|
|
</div>
|
|
{/if}
|
|
</Tabs.Content>
|
|
</Tabs.Root>
|
|
</Dialog.Content>
|
|
</Dialog.Root>
|