diff --git a/.github/workflows/publish-main.yml b/.github/workflows/publish-main.yml index 0560acb2..ef8d7402 100644 --- a/.github/workflows/publish-main.yml +++ b/.github/workflows/publish-main.yml @@ -12,16 +12,16 @@ env: DOCKERHUB_IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} GITHUB_IMAGE_NAME: ${{ github.repository }} -concurrency: - group: main-docker-${{ github.ref }} - cancel-in-progress: true - jobs: build-and-push-main: name: Build and push main Docker images (with docs) strategy: + fail-fast: false matrix: variant: [debian, alpine] + concurrency: + group: main-docker-${{ github.ref }}-${{ matrix.variant }} + cancel-in-progress: true runs-on: ubuntu-latest permissions: contents: read diff --git a/src/lib/clientTools.ts b/src/lib/clientTools.ts index 08d0eabb..7498ebf1 100644 --- a/src/lib/clientTools.ts +++ b/src/lib/clientTools.ts @@ -1,5 +1,6 @@ import type { TimestampStatusCount } from "$lib/server/types/db"; -import GC, { PAGE_STATUS_MESSAGES, type StatusType } from "$lib/global-constants"; +import GC, { PAGE_STATUS_MESSAGES } from "$lib/global-constants"; +import type { StatusType } from "$lib/types/status"; function ParseLatency(latencyMs: number): string { if (!!!latencyMs) { diff --git a/src/lib/global-constants.test.ts b/src/lib/global-constants.test.ts new file mode 100644 index 00000000..bc54c21b --- /dev/null +++ b/src/lib/global-constants.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; +import { isMonitoringStatus } from "./global-constants.js"; + +describe("isMonitoringStatus", () => { + it("accepts only statuses supported by the monitoring filter", () => { + expect(isMonitoringStatus("UP")).toBe(true); + expect(isMonitoringStatus("DOWN")).toBe(true); + expect(isMonitoringStatus("DEGRADED")).toBe(true); + expect(isMonitoringStatus("MAINTENANCE")).toBe(false); + expect(isMonitoringStatus("")).toBe(false); + expect(isMonitoringStatus("invalid")).toBe(false); + }); +}); diff --git a/src/lib/global-constants.ts b/src/lib/global-constants.ts index c18efa76..75d6a9fc 100644 --- a/src/lib/global-constants.ts +++ b/src/lib/global-constants.ts @@ -1,9 +1,15 @@ +import type { MonitoringStatus } from "$lib/types/status.js"; + // Badge styles supported by badge-maker export const BADGE_STYLES = ["flat", "plastic", "flat-square", "for-the-badge", "social"] as const; export type BadgeStyle = (typeof BADGE_STYLES)[number]; // Status types -export type StatusType = "UP" | "DOWN" | "DEGRADED" | "MAINTENANCE" | "NO_DATA"; +export const MONITORING_STATUSES = ["UP", "DOWN", "DEGRADED"] as const satisfies readonly MonitoringStatus[]; + +export function isMonitoringStatus(value: unknown): value is MonitoringStatus { + return typeof value === "string" && MONITORING_STATUSES.includes(value as MonitoringStatus); +} // Incident states export type IncidentState = "INVESTIGATING" | "IDENTIFIED" | "MONITORING" | "RESOLVED"; diff --git a/src/lib/server/api-server/monitor-bar/get.ts b/src/lib/server/api-server/monitor-bar/get.ts index 37d258d6..bb4d56ce 100644 --- a/src/lib/server/api-server/monitor-bar/get.ts +++ b/src/lib/server/api-server/monitor-bar/get.ts @@ -2,7 +2,7 @@ import { json, error } from "@sveltejs/kit"; import type { APIServerRequest } from "$lib/server/types/api-server"; import db from "$lib/server/db/db"; import { GetMinuteStartNowTimestampUTC } from "$lib/server/tool"; -import type { StatusType } from "$lib/global-constants"; +import type { StatusType } from "$lib/types/status"; import type { TimestampStatusCount } from "$lib/server/types/db"; import { buildMonitorBarResponse } from "./shared"; diff --git a/src/lib/server/api-server/monitor-bar/shared.ts b/src/lib/server/api-server/monitor-bar/shared.ts index 4e5bac64..34636227 100644 --- a/src/lib/server/api-server/monitor-bar/shared.ts +++ b/src/lib/server/api-server/monitor-bar/shared.ts @@ -1,5 +1,6 @@ import db from "$lib/server/db/db"; -import GC, { type StatusType } from "$lib/global-constants"; +import GC from "$lib/global-constants"; +import type { StatusType } from "$lib/types/status"; import type { MonitorRecord, TimestampStatusCount } from "$lib/server/types/db"; import { UptimeCalculator } from "$lib/server/tool"; import type { MonitorBarResponse } from "./get"; diff --git a/src/lib/server/api-server/monitor-bars/get.ts b/src/lib/server/api-server/monitor-bars/get.ts index 4ee0ed4f..fab6b91d 100644 --- a/src/lib/server/api-server/monitor-bars/get.ts +++ b/src/lib/server/api-server/monitor-bars/get.ts @@ -2,7 +2,7 @@ import { json, error } from "@sveltejs/kit"; import type { APIServerRequest } from "$lib/server/types/api-server"; import db from "$lib/server/db/db"; import { GetMinuteStartNowTimestampUTC } from "$lib/server/tool"; -import type { StatusType } from "$lib/global-constants"; +import type { StatusType } from "$lib/types/status"; import GC from "$lib/global-constants"; import type { MonitorBarResponse } from "$lib/server/api-server/monitor-bar/get"; import { buildMonitorBarResponseFromRawData } from "$lib/server/api-server/monitor-bar/shared"; diff --git a/src/lib/server/controllers/monitorsController.ts b/src/lib/server/controllers/monitorsController.ts index 65dd6de6..9dc9f8f7 100644 --- a/src/lib/server/controllers/monitorsController.ts +++ b/src/lib/server/controllers/monitorsController.ts @@ -22,6 +22,7 @@ import type { MonitorFilter } from "../db/repositories/base.js"; import db from "../db/db.js"; import type { PaginationInput } from "../../types/common.js"; import GC, { getBadgeStyle, type BadgeStyle } from "../../global-constants.js"; +import type { MonitoringStatus } from "../../types/status.js"; import { makeBadge } from "badge-maker"; import { ErrorSvg } from "../../anywhere.js"; import { GetLastMonitoringValue, SetLastHeartbeat, DeleteMonitorCaches } from "../cache/setGet.js"; @@ -490,7 +491,7 @@ export const GetStatusCountsByInterval = async ( export const GetMonitoringDataPaginated = async ( page: number, limit: number, - filter?: { monitor_tag?: string; start_time?: number; end_time?: number }, + filter?: { monitor_tag?: string; status?: MonitoringStatus; 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); diff --git a/src/lib/server/db/repositories/monitoring.ts b/src/lib/server/db/repositories/monitoring.ts index 0e6c93fa..c558801e 100644 --- a/src/lib/server/db/repositories/monitoring.ts +++ b/src/lib/server/db/repositories/monitoring.ts @@ -1,6 +1,7 @@ import type { Knex as KnexType } from "knex"; import { BaseRepository } from "./base.js"; import GC from "../../../global-constants.js"; +import type { MonitoringStatus } from "../../../types/status.js"; import { GetMinuteStartNowTimestampUTC } from "../../tool.js"; import type { MonitoringData, @@ -82,7 +83,7 @@ export class MonitoringRepository extends BaseRepository { async getMonitoringDataPaginated( page: number, limit: number, - filter?: { monitor_tag?: string; start_time?: number; end_time?: number }, + filter?: { monitor_tag?: string; status?: MonitoringStatus; start_time?: number; end_time?: number }, ): Promise { let query = this.knex("monitoring_data").select("*"); @@ -90,6 +91,10 @@ export class MonitoringRepository extends BaseRepository { query = query.where("monitor_tag", filter.monitor_tag); } + if (filter?.status) { + query = query.where("status", filter.status); + } + if (filter?.start_time) { query = query.where("timestamp", ">=", filter.start_time); } @@ -106,6 +111,7 @@ export class MonitoringRepository extends BaseRepository { async getMonitoringDataCount(filter?: { monitor_tag?: string; + status?: MonitoringStatus; start_time?: number; end_time?: number; }): Promise<{ count: number }> { @@ -115,6 +121,10 @@ export class MonitoringRepository extends BaseRepository { query = query.where("monitor_tag", filter.monitor_tag); } + if (filter?.status) { + query = query.where("status", filter.status); + } + if (filter?.start_time) { query = query.where("timestamp", ">=", filter.start_time); } @@ -476,7 +486,7 @@ export class MonitoringRepository extends BaseRepository { }); } - async deleteMonitorDataByTag(tag?: string, start?: number, end?: number): Promise { + async deleteMonitorDataByTag(tag?: string, start?: number, end?: number, status?: MonitoringStatus): Promise { const query = this.knex("monitoring_data"); if (tag) { query.where("monitor_tag", tag); @@ -487,6 +497,9 @@ export class MonitoringRepository extends BaseRepository { if (end !== undefined) { query.where("timestamp", "<=", end); } + if (status) { + query.where("status", status); + } return await query.del(); } diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 4cae178a..bc83de8a 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -2,3 +2,4 @@ export * from "$lib/types/api"; export * from "$lib/types/monitor"; export * from "$lib/types/result"; export * from "$lib/types/common"; +export * from "$lib/types/status"; diff --git a/src/lib/types/status.ts b/src/lib/types/status.ts new file mode 100644 index 00000000..29c08d15 --- /dev/null +++ b/src/lib/types/status.ts @@ -0,0 +1,2 @@ +export type MonitoringStatus = "UP" | "DOWN" | "DEGRADED"; +export type StatusType = MonitoringStatus | "MAINTENANCE" | "NO_DATA"; diff --git a/src/routes/(kener)/badge/[tag]/dot/+server.ts b/src/routes/(kener)/badge/[tag]/dot/+server.ts index 51348744..883a469c 100644 --- a/src/routes/(kener)/badge/[tag]/dot/+server.ts +++ b/src/routes/(kener)/badge/[tag]/dot/+server.ts @@ -1,7 +1,8 @@ import { GetMonitors, GetLatestMonitoringData, GetLatestStatusActiveAll } from "$lib/server/controllers/controller.js"; import StatusColor, { type StatusColors } from "$lib/color.js"; import { ErrorSvg } from "$lib/anywhere.js"; -import GC, { type StatusType } from "$lib/global-constants.js"; +import GC from "$lib/global-constants.js"; +import type { StatusType } from "$lib/types/status.js"; import { GetLastMonitoringValue } from "$lib/server/cache/setGet.js"; import type { RequestHandler } from "./$types"; diff --git a/src/routes/(manage)/manage/api/+server.ts b/src/routes/(manage)/manage/api/+server.ts index 750fdccd..500d3887 100644 --- a/src/routes/(manage)/manage/api/+server.ts +++ b/src/routes/(manage)/manage/api/+server.ts @@ -4,7 +4,8 @@ import { format } from "date-fns"; import sharp from "sharp"; import { nanoid } from "nanoid"; import db from "$lib/server/db/db"; -import GC from "$lib/global-constants.js"; +import GC, { isMonitoringStatus } from "$lib/global-constants.js"; +import type { MonitoringStatus } from "$lib/types/status.js"; import { CreateUpdateMonitor, UpdateMonitoringData, @@ -219,7 +220,14 @@ export async function POST({ request, cookies }) { } else if (action == "deleteMonitor") { resp = await DeleteMonitorCompletelyUsingTag(data.tag); } else if (action == "deleteMonitorData") { - await db.deleteMonitorDataByTag(data.tag || undefined, data.start, data.end); + let status: MonitoringStatus | undefined; + if (data.status !== undefined && data.status !== null && data.status !== "ALL") { + if (!isMonitoringStatus(data.status)) { + return json({ error: "Invalid monitoring status" }, { status: 400 }); + } + status = data.status; + } + await db.deleteMonitorDataByTag(data.tag || undefined, data.start, data.end, status); resp = { success: true }; } else if (action == "cloneMonitor") { resp = await CloneMonitor({ @@ -245,10 +253,16 @@ export async function POST({ request, cookies }) { } 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 } = {}; + const filter: { monitor_tag?: string; status?: MonitoringStatus; start_time?: number; end_time?: number } = {}; if (data.monitor_tag && data.monitor_tag !== "ALL") { filter.monitor_tag = data.monitor_tag; } + if (data.status !== undefined && data.status !== null && data.status !== "ALL") { + if (!isMonitoringStatus(data.status)) { + return json({ error: "Invalid monitoring status" }, { status: 400 }); + } + filter.status = data.status; + } if (data.start_time) { filter.start_time = parseInt(String(data.start_time)); } diff --git a/src/routes/(manage)/manage/app/monitoring-data/+page.svelte b/src/routes/(manage)/manage/app/monitoring-data/+page.svelte index 5e143537..0dee08f6 100644 --- a/src/routes/(manage)/manage/app/monitoring-data/+page.svelte +++ b/src/routes/(manage)/manage/app/monitoring-data/+page.svelte @@ -19,6 +19,8 @@ import { toast } from "svelte-sonner"; import { resolve } from "$app/paths"; import clientResolver from "$lib/client/resolver.js"; + import GC, { isMonitoringStatus } from "$lib/global-constants"; + import type { MonitoringStatus } from "$lib/types/status.js"; // Types interface MonitoringData { @@ -35,6 +37,8 @@ name: string; } + type StatusFilter = "ALL" | MonitoringStatus; + // 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"); @@ -68,12 +72,14 @@ let pageNo = $state(1); let showFilters = $state(false); let monitorTagFilter = $state("ALL"); + let statusFilter = $state("ALL"); let startDateTime = $state(formatDateTimeForInput(yesterday)); let endDateTime = $state(formatDateTimeForInput(now)); const limit = 50; const hasActiveFilters = $derived( monitorTagFilter !== "ALL" || + statusFilter !== "ALL" || startDateTime !== formatDateTimeForInput(yesterday) || endDateTime !== formatDateTimeForInput(now) ); @@ -113,6 +119,7 @@ function clearFilters() { monitorTagFilter = "ALL"; + statusFilter = "ALL"; startDateTime = formatDateTimeForInput(yesterday); endDateTime = formatDateTimeForInput(now); pageNo = 1; @@ -139,6 +146,7 @@ action: "deleteMonitorData", data: { tag: monitorTagFilter === "ALL" ? "" : monitorTagFilter, + status: statusFilter === "ALL" ? undefined : statusFilter, start: startTs, end: endTs } @@ -189,12 +197,14 @@ page: number; limit: number; monitor_tag: string; + status: StatusFilter; start_time?: number; end_time?: number; } = { page: pageNo, limit, - monitor_tag: monitorTagFilter + monitor_tag: monitorTagFilter, + status: statusFilter }; if (startDateTime) { @@ -241,6 +251,12 @@ } } + function handleStatusChange(value: string | undefined) { + if (value === "ALL" || isMonitoringStatus(value)) { + statusFilter = value; + } + } + // Pagination function goToPage(page: number) { pageNo = page; @@ -304,6 +320,20 @@ +
+ Status + + + {statusFilter === "ALL" ? "All Statuses" : statusFilter} + + + All Statuses + UP + DOWN + DEGRADED + + +