feat(monitors): implement toggle functionality for monitor status and visibility, implements #739

This commit is contained in:
Raj Nandan Sharma
2026-06-06 19:45:00 +05:30
parent 175cf605c6
commit 7e5ea5fda1
2 changed files with 67 additions and 6 deletions
+8
View File
@@ -10,6 +10,14 @@ An open-source status page application providing real-time monitoring, uptime tr
A single check against a service, with a unique tag, a type (API, Ping, TCP, DNS, SSL, SQL, Heartbeat, GameDig, Group, gRPC, None), and a status (ACTIVE or otherwise).
_Avoid_: Check, probe, service
**Inactive Monitor**:
A monitor that is not checked at all: the scheduler drops its job and no monitoring data is collected until it is made ACTIVE again. Independent of visibility (see Hidden Monitor).
_Avoid_: Disabled monitor, paused monitor
**Hidden Monitor**:
A monitor excluded from all status pages while remaining fully checked and alerted. Independent of ACTIVE/INACTIVE.
_Avoid_: Invisible monitor, private monitor
**Group Monitor**:
A monitor whose status is derived from other monitors via a weighted score (UP=1, DEGRADED=0.5, DOWN=0; maintenance counts as UP). A group cannot contain another group.
_Avoid_: Monitor group, composite monitor
@@ -14,15 +14,20 @@
import XIcon from "@lucide/svelte/icons/x";
import { Spinner } from "$lib/components/ui/spinner/index.js";
import * as Item from "$lib/components/ui/item/index.js";
import { Switch } from "$lib/components/ui/switch/index.js";
import type { MonitorRecord } from "$lib/server/types/db.js";
import { resolve } from "$app/paths";
import { page } from "$app/state";
import clientResolver from "$lib/client/resolver.js";
import { GetInitials } from "$lib/clientTools.js";
import { onMount } from "svelte";
import { toast } from "svelte-sonner";
let monitors = $state<MonitorRecord[]>([]);
let loading = $state(true);
let error = $state<string | null>(null);
let toggling = $state<Record<string, boolean>>({});
const canWrite = $derived(page.data.userPermissions?.includes("monitors.write") ?? false);
let showFilters = $state(false);
let statusFilter = $state("ALL");
let searchQuery = $state("");
@@ -93,6 +98,38 @@
}
}
async function toggleMonitorField(monitor: MonitorRecord, field: "status" | "is_hidden", checked: boolean) {
const key = `${monitor.id}:${field}`;
if (toggling[key]) return;
const previous = field === "status" ? monitor.status || "INACTIVE" : monitor.is_hidden || "NO";
const next = field === "status" ? (checked ? "ACTIVE" : "INACTIVE") : checked ? "YES" : "NO";
if (previous === next) return;
// Optimistic flip; rolled back on failure
monitor[field] = next;
toggling[key] = true;
try {
const response = await fetch(clientResolver(resolve, "/manage/api"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "storeMonitorData", data: { ...monitor, [field]: next } })
});
const result = await response.json();
if (result.error) {
throw new Error(result.error);
}
const stateLabel =
field === "status" ? (next === "ACTIVE" ? "active" : "inactive") : next === "YES" ? "hidden" : "visible";
toast.success(`${monitor.name} is now ${stateLabel}`);
} catch (e) {
monitor[field] = previous;
toast.error(e instanceof Error ? e.message : "Failed to update monitor");
} finally {
delete toggling[key];
}
}
onMount(() => {
fetchMonitors();
});
@@ -212,14 +249,30 @@
<Badge variant="secondary">{data.monitor_type}</Badge>
</Table.Cell>
<Table.Cell>
<Badge variant={(data.status || "INACTIVE") === "ACTIVE" ? "default" : "destructive"}>
{data.status || "INACTIVE"}
</Badge>
<div class="flex items-center gap-2">
<Switch
checked={(data.status || "INACTIVE") === "ACTIVE"}
disabled={!canWrite || !!toggling[`${data.id}:status`]}
aria-label="Toggle status for {data.name}"
onCheckedChange={(checked) => toggleMonitorField(data, "status", checked)}
/>
<span class="text-muted-foreground text-xs">
{(data.status || "INACTIVE") === "ACTIVE" ? "Active" : "Inactive"}
</span>
</div>
</Table.Cell>
<Table.Cell>
<Badge variant={data.is_hidden === "YES" ? "destructive" : "outline"}>
{data.is_hidden === "YES" ? "YES" : "NO"}
</Badge>
<div class="flex items-center gap-2">
<Switch
checked={data.is_hidden === "YES"}
disabled={!canWrite || !!toggling[`${data.id}:is_hidden`]}
aria-label="Toggle status page visibility for {data.name}"
onCheckedChange={(checked) => toggleMonitorField(data, "is_hidden", checked)}
/>
<span class="text-muted-foreground text-xs">
{data.is_hidden === "YES" ? "Hidden" : "Visible"}
</span>
</div>
</Table.Cell>
<Table.Cell>
<span class="text-muted-foreground text-xs">{data.cron || "-"}</span>