mirror of
https://github.com/rajnandan1/kener.git
synced 2026-06-23 04:10:22 +00:00
changes
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.text("external_url").nullable();
|
||||
});
|
||||
await knex.schema.alterTable("monitoring_data", (table) => {
|
||||
table.text("error_message").nullable();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.dropColumn("external_url");
|
||||
});
|
||||
await knex.schema.alterTable("monitoring_data", (table) => {
|
||||
table.dropColumn("error_message");
|
||||
});
|
||||
}
|
||||
Generated
+2
-13
@@ -130,8 +130,8 @@
|
||||
}
|
||||
},
|
||||
"../aven": {
|
||||
"name": "@rajnandan1/aven",
|
||||
"version": "0.1.1",
|
||||
"name": "@rajnandan1/atticus",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@openai/agents": "^0.3.7",
|
||||
@@ -7756,17 +7756,6 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.11.7",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
|
||||
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hosted-git-info": {
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import Root from "./range-calendar.svelte";
|
||||
import Cell from "./range-calendar-cell.svelte";
|
||||
import Day from "./range-calendar-day.svelte";
|
||||
import Grid from "./range-calendar-grid.svelte";
|
||||
import Header from "./range-calendar-header.svelte";
|
||||
import Months from "./range-calendar-months.svelte";
|
||||
import GridRow from "./range-calendar-grid-row.svelte";
|
||||
import Heading from "./range-calendar-heading.svelte";
|
||||
import HeadCell from "./range-calendar-head-cell.svelte";
|
||||
import NextButton from "./range-calendar-next-button.svelte";
|
||||
import PrevButton from "./range-calendar-prev-button.svelte";
|
||||
import MonthSelect from "./range-calendar-month-select.svelte";
|
||||
import YearSelect from "./range-calendar-year-select.svelte";
|
||||
import Caption from "./range-calendar-caption.svelte";
|
||||
import Nav from "./range-calendar-nav.svelte";
|
||||
import Month from "./range-calendar-month.svelte";
|
||||
import GridBody from "./range-calendar-grid-body.svelte";
|
||||
import GridHead from "./range-calendar-grid-head.svelte";
|
||||
|
||||
export {
|
||||
Day,
|
||||
Cell,
|
||||
Grid,
|
||||
Header,
|
||||
Months,
|
||||
GridRow,
|
||||
Heading,
|
||||
GridBody,
|
||||
GridHead,
|
||||
HeadCell,
|
||||
NextButton,
|
||||
PrevButton,
|
||||
MonthSelect,
|
||||
YearSelect,
|
||||
Caption,
|
||||
Nav,
|
||||
Month,
|
||||
//
|
||||
Root as RangeCalendar,
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import type { ComponentProps } from "svelte";
|
||||
import type RangeCalendar from "./range-calendar.svelte";
|
||||
import RangeCalendarMonthSelect from "./range-calendar-month-select.svelte";
|
||||
import RangeCalendarYearSelect from "./range-calendar-year-select.svelte";
|
||||
import { DateFormatter, getLocalTimeZone, type DateValue } from "@internationalized/date";
|
||||
|
||||
let {
|
||||
captionLayout,
|
||||
months,
|
||||
monthFormat,
|
||||
years,
|
||||
yearFormat,
|
||||
month,
|
||||
locale,
|
||||
placeholder = $bindable(),
|
||||
monthIndex = 0,
|
||||
}: {
|
||||
captionLayout: ComponentProps<typeof RangeCalendar>["captionLayout"];
|
||||
months: ComponentProps<typeof RangeCalendarMonthSelect>["months"];
|
||||
monthFormat: ComponentProps<typeof RangeCalendarMonthSelect>["monthFormat"];
|
||||
years: ComponentProps<typeof RangeCalendarYearSelect>["years"];
|
||||
yearFormat: ComponentProps<typeof RangeCalendarYearSelect>["yearFormat"];
|
||||
month: DateValue;
|
||||
placeholder: DateValue | undefined;
|
||||
locale: string;
|
||||
monthIndex: number;
|
||||
} = $props();
|
||||
|
||||
function formatYear(date: DateValue) {
|
||||
const dateObj = date.toDate(getLocalTimeZone());
|
||||
if (typeof yearFormat === "function") return yearFormat(dateObj.getFullYear());
|
||||
return new DateFormatter(locale, { year: yearFormat }).format(dateObj);
|
||||
}
|
||||
|
||||
function formatMonth(date: DateValue) {
|
||||
const dateObj = date.toDate(getLocalTimeZone());
|
||||
if (typeof monthFormat === "function") return monthFormat(dateObj.getMonth() + 1);
|
||||
return new DateFormatter(locale, { month: monthFormat }).format(dateObj);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet MonthSelect()}
|
||||
<RangeCalendarMonthSelect
|
||||
{months}
|
||||
{monthFormat}
|
||||
value={month.month}
|
||||
onchange={(e) => {
|
||||
if (!placeholder) return;
|
||||
const v = Number.parseInt(e.currentTarget.value);
|
||||
const newPlaceholder = placeholder.set({ month: v });
|
||||
placeholder = newPlaceholder.subtract({ months: monthIndex });
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet YearSelect()}
|
||||
<RangeCalendarYearSelect {years} {yearFormat} value={month.year} />
|
||||
{/snippet}
|
||||
|
||||
{#if captionLayout === "dropdown"}
|
||||
{@render MonthSelect()}
|
||||
{@render YearSelect()}
|
||||
{:else if captionLayout === "dropdown-months"}
|
||||
{@render MonthSelect()}
|
||||
{#if placeholder}
|
||||
{formatYear(placeholder)}
|
||||
{/if}
|
||||
{:else if captionLayout === "dropdown-years"}
|
||||
{#if placeholder}
|
||||
{formatMonth(placeholder)}
|
||||
{/if}
|
||||
{@render YearSelect()}
|
||||
{:else}
|
||||
{formatMonth(month)} {formatYear(month)}
|
||||
{/if}
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.CellProps = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.Cell
|
||||
bind:ref
|
||||
class={cn(
|
||||
"dark:[&:has([data-range-start])]:hover:bg-accent dark:[&:has([data-range-end])]:hover:bg-accent [&:has([data-range-middle])]:bg-accent dark:[&:has([data-range-middle])]:hover:bg-accent/50 [&:has([data-selected])]:bg-accent relative size-(--cell-size) p-0 text-center text-sm focus-within:z-20 data-[range-middle]:rounded-e-md [&:first-child[data-selected]_[data-bits-day]]:rounded-s-md [&:has([data-range-end])]:rounded-e-md [&:has([data-range-middle])]:rounded-none first:[&:has([data-range-middle])]:rounded-s-md last:[&:has([data-range-middle])]:rounded-e-md [&:has([data-range-start])]:rounded-s-md [&:last-child[data-selected]_[data-bits-day]]:rounded-e-md",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.DayProps = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.Day
|
||||
bind:ref
|
||||
class={cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"flex size-(--cell-size) flex-col items-center justify-center gap-1 p-0 leading-none font-normal whitespace-nowrap select-none",
|
||||
"[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground [&[data-today][data-disabled]]:text-muted-foreground data-[range-middle]:rounded-none",
|
||||
// range Start
|
||||
"data-[range-start]:bg-primary dark:data-[range-start]:hover:bg-accent data-[range-start]:text-primary-foreground",
|
||||
// range End
|
||||
"data-[range-end]:bg-primary dark:data-[range-end]:hover:bg-accent data-[range-end]:text-primary-foreground",
|
||||
// Outside months
|
||||
"[&[data-outside-month]:not([data-selected])]:text-muted-foreground [&[data-outside-month]:not([data-selected])]:hover:text-accent-foreground",
|
||||
// Disabled
|
||||
"data-[disabled]:text-muted-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
// Unavailable
|
||||
"data-[unavailable]:line-through",
|
||||
"dark:data-[range-middle]:hover:bg-accent/0",
|
||||
// hover
|
||||
"dark:hover:text-accent-foreground",
|
||||
// focus
|
||||
"focus:border-ring focus:ring-ring/50 focus:relative",
|
||||
// inner spans
|
||||
"[&>span]:text-xs [&>span]:opacity-70",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: RangeCalendarPrimitive.GridBodyProps = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.GridBody bind:ref {...restProps} />
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: RangeCalendarPrimitive.GridHeadProps = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.GridHead bind:ref {...restProps} />
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.GridRowProps = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.GridRow bind:ref class={cn("flex", className)} {...restProps} />
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.GridProps = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.Grid
|
||||
bind:ref
|
||||
class={cn("mt-4 flex w-full border-collapse flex-col gap-1", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.HeadCellProps = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.HeadCell
|
||||
bind:ref
|
||||
class={cn(
|
||||
"text-muted-foreground w-(--cell-size) rounded-md text-[0.8rem] font-normal",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.HeaderProps = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.Header
|
||||
bind:ref
|
||||
class={cn(
|
||||
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.HeadingProps = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.Heading
|
||||
bind:ref
|
||||
class={cn("px-(--cell-size) text-sm font-medium", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
onchange,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<RangeCalendarPrimitive.MonthSelectProps> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={cn(
|
||||
"has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<RangeCalendarPrimitive.MonthSelect bind:ref class="absolute inset-0 opacity-0" {...restProps}>
|
||||
{#snippet child({ props, monthItems, selectedMonthItem })}
|
||||
<select {...props} {value} {onchange}>
|
||||
{#each monthItems as monthItem (monthItem.value)}
|
||||
<option
|
||||
value={monthItem.value}
|
||||
selected={value !== undefined
|
||||
? monthItem.value === value
|
||||
: monthItem.value === selectedMonthItem.value}
|
||||
>
|
||||
{monthItem.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span
|
||||
class="[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{monthItems.find((item) => item.value === value)?.label || selectedMonthItem.label}
|
||||
<ChevronDownIcon class="size-4" />
|
||||
</span>
|
||||
{/snippet}
|
||||
</RangeCalendarPrimitive.MonthSelect>
|
||||
</span>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { type WithElementRef, cn } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div {...restProps} bind:this={ref} class={cn("flex flex-col", className)}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("relative flex flex-col gap-4 md:flex-row", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<nav
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
class={cn("absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", className)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</nav>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
variant = "ghost",
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.NextButtonProps & {
|
||||
variant?: ButtonVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<ChevronRightIcon class="size-4" />
|
||||
{/snippet}
|
||||
|
||||
<RangeCalendarPrimitive.NextButton
|
||||
bind:ref
|
||||
class={cn(
|
||||
buttonVariants({ variant }),
|
||||
"size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180",
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left";
|
||||
import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
variant = "ghost",
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.PrevButtonProps & {
|
||||
variant?: ButtonVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<ChevronLeftIcon class="size-4" />
|
||||
{/snippet}
|
||||
|
||||
<RangeCalendarPrimitive.PrevButton
|
||||
bind:ref
|
||||
class={cn(
|
||||
buttonVariants({ variant }),
|
||||
"size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180",
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<RangeCalendarPrimitive.YearSelectProps> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={cn(
|
||||
"has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<RangeCalendarPrimitive.YearSelect bind:ref class="absolute inset-0 opacity-0" {...restProps}>
|
||||
{#snippet child({ props, yearItems, selectedYearItem })}
|
||||
<select {...props} {value}>
|
||||
{#each yearItems as yearItem (yearItem.value)}
|
||||
<option
|
||||
value={yearItem.value}
|
||||
selected={value !== undefined
|
||||
? yearItem.value === value
|
||||
: yearItem.value === selectedYearItem.value}
|
||||
>
|
||||
{yearItem.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span
|
||||
class="[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{yearItems.find((item) => item.value === value)?.label || selectedYearItem.label}
|
||||
<ChevronDownIcon class="size-4" />
|
||||
</span>
|
||||
{/snippet}
|
||||
</RangeCalendarPrimitive.YearSelect>
|
||||
</span>
|
||||
@@ -0,0 +1,112 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import * as RangeCalendar from "./index.js";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import type { ButtonVariant } from "$lib/components/ui/button/index.js";
|
||||
import type { Snippet } from "svelte";
|
||||
import { isEqualMonth, type DateValue } from "@internationalized/date";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
placeholder = $bindable(),
|
||||
weekdayFormat = "short",
|
||||
class: className,
|
||||
buttonVariant = "ghost",
|
||||
captionLayout = "label",
|
||||
locale = "en-US",
|
||||
months: monthsProp,
|
||||
years,
|
||||
monthFormat: monthFormatProp,
|
||||
yearFormat = "numeric",
|
||||
day,
|
||||
disableDaysOutsideMonth = false,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<RangeCalendarPrimitive.RootProps> & {
|
||||
buttonVariant?: ButtonVariant;
|
||||
captionLayout?: "dropdown" | "dropdown-months" | "dropdown-years" | "label";
|
||||
months?: RangeCalendarPrimitive.MonthSelectProps["months"];
|
||||
years?: RangeCalendarPrimitive.YearSelectProps["years"];
|
||||
monthFormat?: RangeCalendarPrimitive.MonthSelectProps["monthFormat"];
|
||||
yearFormat?: RangeCalendarPrimitive.YearSelectProps["yearFormat"];
|
||||
day?: Snippet<[{ day: DateValue; outsideMonth: boolean }]>;
|
||||
} = $props();
|
||||
|
||||
const monthFormat = $derived.by(() => {
|
||||
if (monthFormatProp) return monthFormatProp;
|
||||
if (captionLayout.startsWith("dropdown")) return "short";
|
||||
return "long";
|
||||
});
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.Root
|
||||
bind:ref
|
||||
bind:value
|
||||
bind:placeholder
|
||||
{weekdayFormat}
|
||||
{disableDaysOutsideMonth}
|
||||
class={cn(
|
||||
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||
className
|
||||
)}
|
||||
{locale}
|
||||
{monthFormat}
|
||||
{yearFormat}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ months, weekdays })}
|
||||
<RangeCalendar.Months>
|
||||
<RangeCalendar.Nav>
|
||||
<RangeCalendar.PrevButton variant={buttonVariant} />
|
||||
<RangeCalendar.NextButton variant={buttonVariant} />
|
||||
</RangeCalendar.Nav>
|
||||
{#each months as month, monthIndex (month)}
|
||||
<RangeCalendar.Month>
|
||||
<RangeCalendar.Header>
|
||||
<RangeCalendar.Caption
|
||||
{captionLayout}
|
||||
months={monthsProp}
|
||||
{monthFormat}
|
||||
{years}
|
||||
{yearFormat}
|
||||
month={month.value}
|
||||
bind:placeholder
|
||||
{locale}
|
||||
{monthIndex}
|
||||
/>
|
||||
</RangeCalendar.Header>
|
||||
|
||||
<RangeCalendar.Grid>
|
||||
<RangeCalendar.GridHead>
|
||||
<RangeCalendar.GridRow class="select-none">
|
||||
{#each weekdays as weekday (weekday)}
|
||||
<RangeCalendar.HeadCell>
|
||||
{weekday.slice(0, 2)}
|
||||
</RangeCalendar.HeadCell>
|
||||
{/each}
|
||||
</RangeCalendar.GridRow>
|
||||
</RangeCalendar.GridHead>
|
||||
<RangeCalendar.GridBody>
|
||||
{#each month.weeks as weekDates (weekDates)}
|
||||
<RangeCalendar.GridRow class="mt-2 w-full">
|
||||
{#each weekDates as date (date)}
|
||||
<RangeCalendar.Cell {date} month={month.value}>
|
||||
{#if day}
|
||||
{@render day({
|
||||
day: date,
|
||||
outsideMonth: !isEqualMonth(date, month.value),
|
||||
})}
|
||||
{:else}
|
||||
<RangeCalendar.Day />
|
||||
{/if}
|
||||
</RangeCalendar.Cell>
|
||||
{/each}
|
||||
</RangeCalendar.GridRow>
|
||||
{/each}
|
||||
</RangeCalendar.GridBody>
|
||||
</RangeCalendar.Grid>
|
||||
</RangeCalendar.Month>
|
||||
{/each}
|
||||
</RangeCalendar.Months>
|
||||
{/snippet}
|
||||
</RangeCalendarPrimitive.Root>
|
||||
@@ -70,6 +70,7 @@ interface MonitoringDataInput {
|
||||
status: string;
|
||||
latency?: number;
|
||||
type: string;
|
||||
error_message?: string | null;
|
||||
}
|
||||
|
||||
interface InterpolatedDataEntry {
|
||||
@@ -98,6 +99,7 @@ export const InsertMonitoringData = async (data: MonitoringDataInput): Promise<n
|
||||
status: data.status,
|
||||
latency: data.latency || 0,
|
||||
type: data.type,
|
||||
error_message: data.error_message,
|
||||
});
|
||||
};
|
||||
export const ProcessGroupUpdate = async (data: GroupUpdateData): Promise<void> => {
|
||||
@@ -531,6 +533,17 @@ export const GetStatusCountsByInterval = async (
|
||||
return await db.getStatusCountsByInterval(monitor_tag, start, interval, numIntervals);
|
||||
};
|
||||
|
||||
//getMonitoringDataPaginated
|
||||
export const GetMonitoringDataPaginated = async (
|
||||
page: number,
|
||||
limit: number,
|
||||
filter?: { monitor_tag?: string; start_time?: number; end_time?: number },
|
||||
): Promise<{ data: MonitoringData[]; total: number }> => {
|
||||
const data = await db.getMonitoringDataPaginated(page, limit, filter);
|
||||
const countResult = await db.getMonitoringDataCount(filter);
|
||||
return { data, total: countResult.count };
|
||||
};
|
||||
|
||||
export type BadgeType = "status" | "uptime" | "latency";
|
||||
|
||||
export interface BadgeParams {
|
||||
|
||||
@@ -53,6 +53,8 @@ class DbImpl {
|
||||
getMonitoringDataAll!: MonitoringRepository["getMonitoringDataAll"];
|
||||
getLatestMonitoringData!: MonitoringRepository["getLatestMonitoringData"];
|
||||
getLatestMonitoringDataN!: MonitoringRepository["getLatestMonitoringDataN"];
|
||||
getMonitoringDataPaginated!: MonitoringRepository["getMonitoringDataPaginated"];
|
||||
getMonitoringDataCount!: MonitoringRepository["getMonitoringDataCount"];
|
||||
getMonitoringDataAt!: MonitoringRepository["getMonitoringDataAt"];
|
||||
getLatestMonitoringDataAllActive!: MonitoringRepository["getLatestMonitoringDataAllActive"];
|
||||
getLastHeartbeat!: MonitoringRepository["getLastHeartbeat"];
|
||||
@@ -391,6 +393,8 @@ class DbImpl {
|
||||
this.getMonitoringDataAll = this.monitoring.getMonitoringDataAll.bind(this.monitoring);
|
||||
this.getLatestMonitoringData = this.monitoring.getLatestMonitoringData.bind(this.monitoring);
|
||||
this.getLatestMonitoringDataN = this.monitoring.getLatestMonitoringDataN.bind(this.monitoring);
|
||||
this.getMonitoringDataPaginated = this.monitoring.getMonitoringDataPaginated.bind(this.monitoring);
|
||||
this.getMonitoringDataCount = this.monitoring.getMonitoringDataCount.bind(this.monitoring);
|
||||
this.getMonitoringDataAt = this.monitoring.getMonitoringDataAt.bind(this.monitoring);
|
||||
this.getLatestMonitoringDataAllActive = this.monitoring.getLatestMonitoringDataAllActive.bind(this.monitoring);
|
||||
this.getLastHeartbeat = this.monitoring.getLastHeartbeat.bind(this.monitoring);
|
||||
|
||||
@@ -14,11 +14,11 @@ import type {
|
||||
*/
|
||||
export class MonitoringRepository extends BaseRepository {
|
||||
async insertMonitoringData(data: MonitoringDataInsert): Promise<number[]> {
|
||||
const { monitor_tag, timestamp, status, latency, type } = data;
|
||||
const { monitor_tag, timestamp, status, latency, type, error_message } = data;
|
||||
return await this.knex("monitoring_data")
|
||||
.insert({ monitor_tag, timestamp, status, latency, type })
|
||||
.insert({ monitor_tag, timestamp, status, latency, type, error_message })
|
||||
.onConflict(["monitor_tag", "timestamp"])
|
||||
.merge({ status, latency, type });
|
||||
.merge({ status, latency, type, error_message });
|
||||
}
|
||||
|
||||
async getMonitoringData(monitor_tag: string, start: number, end: number): Promise<MonitoringData[]> {
|
||||
@@ -65,6 +65,50 @@ export class MonitoringRepository extends BaseRepository {
|
||||
.limit(limit);
|
||||
}
|
||||
|
||||
async getMonitoringDataPaginated(
|
||||
page: number,
|
||||
limit: number,
|
||||
filter?: { monitor_tag?: string; start_time?: number; end_time?: number },
|
||||
): Promise<MonitoringData[]> {
|
||||
let query = this.knex("monitoring_data").select("*");
|
||||
|
||||
if (filter?.monitor_tag) {
|
||||
query = query.where("monitor_tag", filter.monitor_tag);
|
||||
}
|
||||
|
||||
if (filter?.start_time) {
|
||||
query = query.where("timestamp", ">=", filter.start_time);
|
||||
}
|
||||
|
||||
if (filter?.end_time) {
|
||||
query = query.where("timestamp", "<=", filter.end_time);
|
||||
}
|
||||
|
||||
return await query
|
||||
.orderBy("timestamp", "desc")
|
||||
.limit(limit)
|
||||
.offset((page - 1) * limit);
|
||||
}
|
||||
|
||||
async getMonitoringDataCount(filter?: { monitor_tag?: string; start_time?: number; end_time?: number }): Promise<{ count: number }> {
|
||||
let query = this.knex("monitoring_data").count("* as count");
|
||||
|
||||
if (filter?.monitor_tag) {
|
||||
query = query.where("monitor_tag", filter.monitor_tag);
|
||||
}
|
||||
|
||||
if (filter?.start_time) {
|
||||
query = query.where("timestamp", ">=", filter.start_time);
|
||||
}
|
||||
|
||||
if (filter?.end_time) {
|
||||
query = query.where("timestamp", "<=", filter.end_time);
|
||||
}
|
||||
|
||||
const result = await query.first();
|
||||
return { count: Number(result?.count) || 0 };
|
||||
}
|
||||
|
||||
async getMonitoringDataAt(monitor_tag: string, timestamp: number): Promise<MonitoringData | undefined> {
|
||||
return await this.knex("monitoring_data")
|
||||
.where("monitor_tag", monitor_tag)
|
||||
|
||||
@@ -65,6 +65,7 @@ async function manualMaintenance(monitor: MonitorRecordTyped): Promise<{ [timest
|
||||
status: impact,
|
||||
latency: 0,
|
||||
type: GC.MANUAL,
|
||||
error_message: "Status set by manual maintenance",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -105,6 +106,7 @@ async function manualIncident(monitor: MonitorRecordTyped): Promise<{ [timestamp
|
||||
status: impact,
|
||||
latency: 0,
|
||||
type: GC.MANUAL,
|
||||
error_message: "Status set by manual incident",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -118,27 +120,28 @@ const addWorker = () => {
|
||||
const serviceClient = new Service(monitor as MonitorWithType);
|
||||
|
||||
const exeResult = await serviceClient.execute(ts);
|
||||
if (exeResult && exeResult.type === GC.TIMEOUT && monitor.monitor_type === "API") {
|
||||
let countTimeoutRetries = executeOptions?.countTimeoutRetries || 0;
|
||||
let maxTimeoutRetries = executeOptions?.maxTimeoutRetries || 3;
|
||||
if (countTimeoutRetries < maxTimeoutRetries) {
|
||||
console.log(
|
||||
`Timeout detected for monitor ${monitor.tag} at ${ts}. Retrying ${countTimeoutRetries + 1}/${maxTimeoutRetries}...`,
|
||||
);
|
||||
await push(
|
||||
monitor,
|
||||
ts,
|
||||
{
|
||||
...executeOptions,
|
||||
countTimeoutRetries: countTimeoutRetries + 1,
|
||||
},
|
||||
{ delay: 500 },
|
||||
);
|
||||
return {
|
||||
[ts]: exeResult,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// if (exeResult && exeResult.type === GC.TIMEOUT && monitor.monitor_type === "API") {
|
||||
// let countTimeoutRetries = executeOptions?.countTimeoutRetries || 0;
|
||||
// let maxTimeoutRetries = executeOptions?.maxTimeoutRetries || 3;
|
||||
// if (countTimeoutRetries < maxTimeoutRetries) {
|
||||
// console.log(
|
||||
// `Timeout detected for monitor ${monitor.tag} at ${ts}. Retrying ${countTimeoutRetries + 1}/${maxTimeoutRetries}...`,
|
||||
// );
|
||||
// await push(
|
||||
// monitor,
|
||||
// ts,
|
||||
// {
|
||||
// ...executeOptions,
|
||||
// countTimeoutRetries: countTimeoutRetries + 1,
|
||||
// },
|
||||
// { delay: 500 },
|
||||
// );
|
||||
// return {
|
||||
// [ts]: exeResult,
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
||||
let realtimeData: MonitoringResultTS = {};
|
||||
if (exeResult) {
|
||||
@@ -156,10 +159,35 @@ const addWorker = () => {
|
||||
latency: 0,
|
||||
type: GC.DEFAULT_STATUS,
|
||||
};
|
||||
if (monitor.default_status !== GC.UP) {
|
||||
defaultData[ts].error_message = "Default status applied";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge data: later entries override earlier ones for all fields except error_message
|
||||
mergedData = { ...defaultData, ...realtimeData, ...incidentData, ...maintenanceData };
|
||||
|
||||
// Preserve error_message with cascading priority:
|
||||
// default → realtime → incident → maintenance
|
||||
// Each level only overrides if it has its own error_message
|
||||
for (const timestamp in mergedData) {
|
||||
const ts = parseInt(timestamp);
|
||||
let errorMessage: string | undefined = defaultData[ts]?.error_message;
|
||||
if (realtimeData[ts]?.error_message) {
|
||||
errorMessage = realtimeData[ts].error_message;
|
||||
}
|
||||
if (incidentData[ts]?.error_message) {
|
||||
errorMessage = incidentData[ts].error_message;
|
||||
}
|
||||
if (maintenanceData[ts]?.error_message) {
|
||||
errorMessage = maintenanceData[ts].error_message;
|
||||
}
|
||||
if (errorMessage) {
|
||||
mergedData[ts].error_message = errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in mergedData) {
|
||||
monitorResponseQueue.push(monitor.tag, parseInt(timestamp), mergedData[timestamp]);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ interface JobData {
|
||||
type: string;
|
||||
monitorTag: string;
|
||||
ts: number;
|
||||
error_message?: string | null;
|
||||
}
|
||||
|
||||
const getQueue = () => {
|
||||
@@ -28,7 +29,7 @@ const addWorker = () => {
|
||||
if (worker) return worker;
|
||||
|
||||
worker = q.createWorker(getQueue(), async (job: Job): Promise<number[]> => {
|
||||
const { monitorTag, ts, status, latency, type } = job.data as JobData;
|
||||
const { monitorTag, ts, status, latency, type, error_message } = job.data as JobData;
|
||||
|
||||
const dbRes = await InsertMonitoringData({
|
||||
monitor_tag: monitorTag,
|
||||
@@ -36,6 +37,7 @@ const addWorker = () => {
|
||||
status: status,
|
||||
latency: latency,
|
||||
type: type,
|
||||
error_message: error_message,
|
||||
});
|
||||
|
||||
if (dbRes.length > 0) {
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface MonitoringData {
|
||||
status: string | null;
|
||||
latency: number | null;
|
||||
type: string | null;
|
||||
error_message?: string | null;
|
||||
}
|
||||
|
||||
export interface MonitoringDataInsert {
|
||||
@@ -16,6 +17,7 @@ export interface MonitoringDataInsert {
|
||||
status: string;
|
||||
latency: number;
|
||||
type: string;
|
||||
error_message?: string | null;
|
||||
}
|
||||
|
||||
export interface AggregatedMonitoringData {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
import TemplateIcon from "@lucide/svelte/icons/layout-template";
|
||||
import EmailTemplateIcon from "@lucide/svelte/icons/mail-plus";
|
||||
import VaultIcon from "@lucide/svelte/icons/vault";
|
||||
import DatabaseIcon from "@lucide/svelte/icons/database";
|
||||
|
||||
import { Toaster } from "$lib/components/ui/sonner/index.js";
|
||||
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||
@@ -54,7 +55,8 @@
|
||||
{ title: "Embed", url: "/manage/app/embed", icon: CodeIcon },
|
||||
{ title: "Templates", url: "/manage/app/templates", icon: TemplateIcon },
|
||||
{ title: "Email Customization", url: "/manage/app/email-customization", icon: EmailTemplateIcon },
|
||||
{ title: "Vault", url: "/manage/app/vault", icon: VaultIcon }
|
||||
{ title: "Vault", url: "/manage/app/vault", icon: VaultIcon },
|
||||
{ title: "Database", url: "/manage/app/monitoring-data", icon: DatabaseIcon }
|
||||
];
|
||||
|
||||
// Derive page title from current URL
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
ManualUpdateUserData,
|
||||
DeleteMonitorCompletelyUsingTag,
|
||||
GetSiteDataByKey,
|
||||
GetMonitoringDataPaginated,
|
||||
} from "$lib/server/controllers/controller.js";
|
||||
|
||||
import { INVITE_VERIFY_EMAIL, MANUAL } from "$lib/server/constants.js";
|
||||
@@ -256,6 +257,20 @@ export async function POST({ request, cookies }) {
|
||||
const limit = parseInt(String(data.limit)) || 20;
|
||||
const filter = data.status && data.status !== "ALL" ? { alert_status: data.status } : undefined;
|
||||
resp = await GetMonitorAlertsV2Paginated(page, limit, filter);
|
||||
} else if (action == "getMonitoringDataPaginated") {
|
||||
const page = parseInt(String(data.page)) || 1;
|
||||
const limit = parseInt(String(data.limit)) || 50;
|
||||
const filter: { monitor_tag?: string; start_time?: number; end_time?: number } = {};
|
||||
if (data.monitor_tag && data.monitor_tag !== "ALL") {
|
||||
filter.monitor_tag = data.monitor_tag;
|
||||
}
|
||||
if (data.start_time) {
|
||||
filter.start_time = parseInt(String(data.start_time));
|
||||
}
|
||||
if (data.end_time) {
|
||||
filter.end_time = parseInt(String(data.end_time));
|
||||
}
|
||||
resp = await GetMonitoringDataPaginated(page, limit, Object.keys(filter).length > 0 ? filter : undefined);
|
||||
} else if (action == "getAPIKeys") {
|
||||
resp = await GetAllAPIKeys();
|
||||
} else if (action == "createNewApiKey") {
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { Spinner } from "$lib/components/ui/spinner/index.js";
|
||||
import { Badge } from "$lib/components/ui/badge/index.js";
|
||||
import * as Table from "$lib/components/ui/table/index.js";
|
||||
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||
import * as Select from "$lib/components/ui/select/index.js";
|
||||
import { Input } from "$lib/components/ui/input/index.js";
|
||||
import { Label } from "$lib/components/ui/label/index.js";
|
||||
import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left";
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import { format } from "date-fns";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
// Types
|
||||
interface MonitoringData {
|
||||
monitor_tag: string;
|
||||
timestamp: number;
|
||||
status: string | null;
|
||||
latency: number | null;
|
||||
type: string | null;
|
||||
error_message?: string | null;
|
||||
}
|
||||
|
||||
interface Monitor {
|
||||
tag: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Helper to format datetime as YYYY-MM-DDTHH:mm for datetime-local input
|
||||
function formatDateTimeForInput(date: Date): string {
|
||||
return format(date, "yyyy-MM-dd'T'HH:mm");
|
||||
}
|
||||
|
||||
// Helper to format date only as YYYY-MM-DD for min/max constraints
|
||||
function formatDateForInput(date: Date): string {
|
||||
return format(date, "yyyy-MM-dd");
|
||||
}
|
||||
|
||||
// Helper to get date N days ago
|
||||
function getDaysAgo(days: number): Date {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - days);
|
||||
return date;
|
||||
}
|
||||
|
||||
// Default date range: last 24 hours
|
||||
const now = new Date();
|
||||
const yesterday = getDaysAgo(1);
|
||||
const maxDaysAgoDate = getDaysAgo(30);
|
||||
|
||||
// State
|
||||
let loading = $state(true);
|
||||
let monitoringData = $state<MonitoringData[]>([]);
|
||||
let monitors = $state<Monitor[]>([]);
|
||||
let totalPages = $state(0);
|
||||
let totalCount = $state(0);
|
||||
let pageNo = $state(1);
|
||||
let monitorTagFilter = $state("ALL");
|
||||
let startDateTime = $state(formatDateTimeForInput(yesterday));
|
||||
let endDateTime = $state(formatDateTimeForInput(now));
|
||||
const limit = 50;
|
||||
|
||||
// Convert datetime string (YYYY-MM-DDTHH:mm) to Unix timestamp (seconds)
|
||||
function dateTimeStringToTimestamp(dateTimeStr: string): number {
|
||||
const date = new Date(dateTimeStr);
|
||||
return Math.floor(date.getTime() / 1000);
|
||||
}
|
||||
|
||||
// Validate date range (max 30 days)
|
||||
function validateAndFetch() {
|
||||
const start = new Date(startDateTime);
|
||||
const end = new Date(endDateTime);
|
||||
|
||||
// Ensure start is before end
|
||||
if (start > end) {
|
||||
startDateTime = endDateTime;
|
||||
}
|
||||
|
||||
// Check if range exceeds 30 days
|
||||
const daysDiff = Math.abs((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
|
||||
if (daysDiff > 30) {
|
||||
// Adjust end datetime to be 30 days from start
|
||||
const newEnd = new Date(start);
|
||||
newEnd.setDate(newEnd.getDate() + 30);
|
||||
if (newEnd > now) {
|
||||
endDateTime = formatDateTimeForInput(now);
|
||||
} else {
|
||||
endDateTime = formatDateTimeForInput(newEnd);
|
||||
}
|
||||
}
|
||||
|
||||
pageNo = 1;
|
||||
fetchData();
|
||||
}
|
||||
|
||||
// Fetch monitors for filter dropdown
|
||||
async function fetchMonitors() {
|
||||
try {
|
||||
const response = await fetch("/manage/api", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "getMonitors",
|
||||
data: {}
|
||||
})
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!result.error && Array.isArray(result)) {
|
||||
monitors = result.map((m: { tag: string; name: string }) => ({ tag: m.tag, name: m.name }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching monitors:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch monitoring data
|
||||
async function fetchData() {
|
||||
loading = true;
|
||||
try {
|
||||
const requestData: {
|
||||
page: number;
|
||||
limit: number;
|
||||
monitor_tag: string;
|
||||
start_time?: number;
|
||||
end_time?: number;
|
||||
} = {
|
||||
page: pageNo,
|
||||
limit,
|
||||
monitor_tag: monitorTagFilter
|
||||
};
|
||||
|
||||
if (startDateTime) {
|
||||
requestData.start_time = dateTimeStringToTimestamp(startDateTime);
|
||||
}
|
||||
if (endDateTime) {
|
||||
requestData.end_time = dateTimeStringToTimestamp(endDateTime);
|
||||
}
|
||||
|
||||
const response = await fetch("/manage/api", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "getMonitoringDataPaginated",
|
||||
data: requestData
|
||||
})
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!result.error) {
|
||||
monitoringData = result.data as MonitoringData[];
|
||||
totalCount = result.total;
|
||||
totalPages = Math.ceil(result.total / limit);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching monitoring data:", error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get badge variant for status
|
||||
function getStatusBadgeVariant(status: string | null): "default" | "secondary" | "destructive" | "outline" {
|
||||
switch (status) {
|
||||
case "DOWN":
|
||||
return "destructive";
|
||||
case "DEGRADED":
|
||||
return "outline";
|
||||
case "UP":
|
||||
return "default";
|
||||
default:
|
||||
return "secondary";
|
||||
}
|
||||
}
|
||||
|
||||
// Format timestamp to date string
|
||||
function formatTimestamp(timestamp: number): string {
|
||||
try {
|
||||
const date = new Date(timestamp * 1000);
|
||||
return format(date, "yyyy-MM-dd HH:mm:ss");
|
||||
} catch {
|
||||
return String(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle monitor tag filter change
|
||||
function handleMonitorChange(value: string | undefined) {
|
||||
if (value) {
|
||||
monitorTagFilter = value;
|
||||
pageNo = 1;
|
||||
fetchData();
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination
|
||||
function goToPage(page: number) {
|
||||
pageNo = page;
|
||||
fetchData();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchMonitors();
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto space-y-6 py-6">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<h1 class="text-2xl font-semibold">Monitoring Data</h1>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<!-- Date Time Range Inputs -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Label for="start-datetime" class="text-sm text-muted-foreground">From</Label>
|
||||
<Input
|
||||
id="start-datetime"
|
||||
type="datetime-local"
|
||||
bind:value={startDateTime}
|
||||
onchange={validateAndFetch}
|
||||
min={formatDateTimeForInput(maxDaysAgoDate)}
|
||||
max={endDateTime}
|
||||
class="w-44"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Label for="end-datetime" class="text-sm text-muted-foreground">To</Label>
|
||||
<Input
|
||||
id="end-datetime"
|
||||
type="datetime-local"
|
||||
bind:value={endDateTime}
|
||||
onchange={validateAndFetch}
|
||||
min={startDateTime}
|
||||
max={formatDateTimeForInput(now)}
|
||||
class="w-44"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monitor Filter -->
|
||||
<Select.Root type="single" value={monitorTagFilter} onValueChange={handleMonitorChange}>
|
||||
<Select.Trigger class="w-48">
|
||||
{monitorTagFilter === "ALL" ? "All Monitors" : monitorTagFilter}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="ALL">All Monitors</Select.Item>
|
||||
{#each monitors as monitor (monitor.tag)}
|
||||
<Select.Item value={monitor.tag}>{monitor.name || monitor.tag}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{#if loading}
|
||||
<Spinner class="size-5" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<div class="ktable rounded-xl border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head>Monitor Tag</Table.Head>
|
||||
<Table.Head class="w-48">Timestamp</Table.Head>
|
||||
<Table.Head class="w-24">Status</Table.Head>
|
||||
<Table.Head class="w-24">Latency</Table.Head>
|
||||
<Table.Head class="w-24">Type</Table.Head>
|
||||
<Table.Head>Error Message</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if monitoringData.length === 0 && !loading}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={6} class="text-muted-foreground py-8 text-center">No monitoring data found</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each monitoringData as row (row.monitor_tag + '_' + row.timestamp)}
|
||||
<Table.Row class="hover:bg-muted/50">
|
||||
<Table.Cell>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<span class="line-clamp-1 max-w-xs font-medium">{row.monitor_tag}</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>{row.monitor_tag}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<span class="text-muted-foreground text-sm">{formatTimestamp(row.timestamp)}</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge variant={getStatusBadgeVariant(row.status)}>
|
||||
{row.status || "N/A"}
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#if row.latency !== null}
|
||||
<span class="text-sm">{row.latency} ms</span>
|
||||
{:else}
|
||||
<span class="text-muted-foreground text-sm">—</span>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#if row.type}
|
||||
<Badge variant="secondary">{row.type}</Badge>
|
||||
{:else}
|
||||
<span class="text-muted-foreground text-sm">—</span>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#if row.error_message}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<span class="text-destructive line-clamp-1 max-w-xs text-sm">{row.error_message}</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<p class="break-words">{row.error_message}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{:else}
|
||||
<span class="text-muted-foreground text-sm">—</span>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{#if totalCount > 0}
|
||||
{@const startItem = (pageNo - 1) * limit + 1}
|
||||
{@const endItem = Math.min(pageNo * limit, totalCount)}
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground text-sm">Showing {startItem}-{endItem} of {totalCount}</span>
|
||||
{#if totalPages > 1}
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" disabled={pageNo === 1} onclick={() => goToPage(pageNo - 1)}>
|
||||
<ChevronLeftIcon class="size-4" />
|
||||
</Button>
|
||||
<div class="flex items-center gap-1">
|
||||
{#each Array.from({ length: totalPages }, (_, i) => i + 1) as page (page)}
|
||||
{#if page === 1 || page === totalPages || (page >= pageNo - 1 && page <= pageNo + 1)}
|
||||
<Button variant={page === pageNo ? "default" : "ghost"} size="sm" onclick={() => goToPage(page)}>
|
||||
{page}
|
||||
</Button>
|
||||
{:else if page === pageNo - 2 || page === pageNo + 2}
|
||||
<span class="text-muted-foreground px-1">...</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<Button variant="outline" size="icon" disabled={pageNo === totalPages} onclick={() => goToPage(pageNo + 1)}>
|
||||
<ChevronRightIcon class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -31,6 +31,7 @@
|
||||
import * as InputGroup from "$lib/components/ui/input-group/index.js";
|
||||
import type { MonitoringResult } from "$lib/server/types/monitor.js";
|
||||
import MonitorAlerting from "./components/MonitorAlerting.svelte";
|
||||
import MonitorRecentLogs from "./components/MonitorRecentLogs.svelte";
|
||||
// Type-specific components
|
||||
import {
|
||||
MonitorApi,
|
||||
@@ -1097,6 +1098,10 @@
|
||||
{#if !isNew}
|
||||
<MonitorAlerting monitor_tag={params.tag} />
|
||||
{/if}
|
||||
|
||||
{#if !isNew}
|
||||
<MonitorRecentLogs monitor_tag={params.tag} />
|
||||
{/if}
|
||||
<!-- Pages Card -->
|
||||
{#if !isNew}
|
||||
<Card.Root>
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
<script lang="ts">
|
||||
import { Badge } from "$lib/components/ui/badge/index.js";
|
||||
import { Spinner } from "$lib/components/ui/spinner/index.js";
|
||||
import * as Card from "$lib/components/ui/card/index.js";
|
||||
import * as Table from "$lib/components/ui/table/index.js";
|
||||
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import RefreshCwIcon from "@lucide/svelte/icons/refresh-cw";
|
||||
import ExternalLinkIcon from "@lucide/svelte/icons/external-link";
|
||||
import { format } from "date-fns";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
interface Props {
|
||||
monitor_tag: string;
|
||||
}
|
||||
|
||||
let { monitor_tag }: Props = $props();
|
||||
|
||||
// Types
|
||||
interface MonitoringData {
|
||||
monitor_tag: string;
|
||||
timestamp: number;
|
||||
status: string | null;
|
||||
latency: number | null;
|
||||
type: string | null;
|
||||
error_message?: string | null;
|
||||
}
|
||||
|
||||
// State
|
||||
let loading = $state(true);
|
||||
let logs = $state<MonitoringData[]>([]);
|
||||
|
||||
// Fetch last 10 logs for this monitor
|
||||
async function fetchLogs() {
|
||||
loading = true;
|
||||
try {
|
||||
const response = await fetch("/manage/api", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "getMonitoringDataPaginated",
|
||||
data: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
monitor_tag: monitor_tag
|
||||
}
|
||||
})
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!result.error) {
|
||||
logs = result.data as MonitoringData[];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching monitoring logs:", error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get badge variant for status
|
||||
function getStatusBadgeVariant(status: string | null): "default" | "secondary" | "destructive" | "outline" {
|
||||
switch (status) {
|
||||
case "DOWN":
|
||||
return "destructive";
|
||||
case "DEGRADED":
|
||||
return "outline";
|
||||
case "UP":
|
||||
return "default";
|
||||
default:
|
||||
return "secondary";
|
||||
}
|
||||
}
|
||||
|
||||
// Format timestamp to date string
|
||||
function formatTimestamp(timestamp: number): string {
|
||||
try {
|
||||
const date = new Date(timestamp * 1000);
|
||||
return format(date, "yyyy-MM-dd HH:mm:ss");
|
||||
} catch {
|
||||
return String(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchLogs();
|
||||
});
|
||||
|
||||
// Refetch when monitor_tag changes
|
||||
$effect(() => {
|
||||
if (monitor_tag) {
|
||||
fetchLogs();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Recent Logs</Card.Title>
|
||||
<Card.Description>Last 10 monitoring data points</Card.Description>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onclick={fetchLogs} disabled={loading}>
|
||||
<RefreshCwIcon class="size-4 {loading ? 'animate-spin' : ''}" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" href="/manage/app/monitoring-data">
|
||||
View All
|
||||
<ExternalLinkIcon class="ml-1 size-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
{#if loading && logs.length === 0}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<Spinner class="size-6" />
|
||||
</div>
|
||||
{:else if logs.length === 0}
|
||||
<div class="text-muted-foreground py-8 text-center text-sm">
|
||||
No monitoring data found for this monitor
|
||||
</div>
|
||||
{:else}
|
||||
<div class="ktable rounded-lg border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head class="w-44">Timestamp</Table.Head>
|
||||
<Table.Head class="w-20">Status</Table.Head>
|
||||
<Table.Head class="w-20">Latency</Table.Head>
|
||||
<Table.Head class="w-20">Type</Table.Head>
|
||||
<Table.Head>Error</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each logs as log (log.timestamp)}
|
||||
<Table.Row class="hover:bg-muted/50">
|
||||
<Table.Cell>
|
||||
<span class="text-muted-foreground text-sm">{formatTimestamp(log.timestamp)}</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge variant={getStatusBadgeVariant(log.status)}>
|
||||
{log.status || "N/A"}
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#if log.latency !== null}
|
||||
<span class="text-sm">{log.latency} ms</span>
|
||||
{:else}
|
||||
<span class="text-muted-foreground text-sm">—</span>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#if log.type}
|
||||
<Badge variant="secondary" class="text-xs">{log.type}</Badge>
|
||||
{:else}
|
||||
<span class="text-muted-foreground text-sm">—</span>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#if log.error_message}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<span class="text-destructive line-clamp-1 max-w-xs text-sm">{log.error_message}</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<p class="break-words">{log.error_message}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{:else}
|
||||
<span class="text-muted-foreground text-sm">—</span>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
Reference in New Issue
Block a user