This commit is contained in:
Raj Nandan Sharma
2026-02-02 19:01:44 +05:30
parent d851610432
commit 00511da24c
32 changed files with 1287 additions and 39 deletions
@@ -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");
});
}
+2 -13
View File
@@ -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 {
+4
View File
@@ -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);
+47 -3
View File
@@ -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)
+49 -21
View File
@@ -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) {
+2
View File
@@ -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 {
+3 -1
View File
@@ -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
+15
View File
@@ -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>