ui: refactor heartbeat settings page

This commit is contained in:
henrygd
2026-02-20 14:42:41 -05:00
parent aa8b3711d7
commit 191f25f6e0
31 changed files with 106 additions and 122 deletions

View File

@@ -1,7 +1,6 @@
import { t } from "@lingui/core/macro" import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { redirectPage } from "@nanostores/router" import { redirectPage } from "@nanostores/router"
import clsx from "clsx"
import { LoaderCircleIcon, SendIcon } from "lucide-react" import { LoaderCircleIcon, SendIcon } from "lucide-react"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { $router } from "@/components/router" import { $router } from "@/components/router"
@@ -10,6 +9,7 @@ import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { toast } from "@/components/ui/use-toast" import { toast } from "@/components/ui/use-toast"
import { isAdmin, pb } from "@/lib/api" import { isAdmin, pb } from "@/lib/api"
import { cn } from "@/lib/utils"
interface HeartbeatStatus { interface HeartbeatStatus {
enabled: boolean enabled: boolean
@@ -37,10 +37,10 @@ export default function HeartbeatSettings() {
setIsLoading(true) setIsLoading(true)
const res = await pb.send<HeartbeatStatus>("/api/beszel/heartbeat-status", {}) const res = await pb.send<HeartbeatStatus>("/api/beszel/heartbeat-status", {})
setStatus(res) setStatus(res)
} catch (error: any) { } catch (error: unknown) {
toast({ toast({
title: t`Error`, title: t`Error`,
description: error.message, description: (error as Error).message,
variant: "destructive", variant: "destructive",
}) })
} finally { } finally {
@@ -66,10 +66,10 @@ export default function HeartbeatSettings() {
variant: "destructive", variant: "destructive",
}) })
} }
} catch (error: any) { } catch (error: unknown) {
toast({ toast({
title: t`Error`, title: t`Error`,
description: error.message, description: (error as Error).message,
variant: "destructive", variant: "destructive",
}) })
} finally { } finally {
@@ -77,8 +77,6 @@ export default function HeartbeatSettings() {
} }
} }
const TestIcon = isTesting ? LoaderCircleIcon : SendIcon
return ( return (
<div> <div>
<div> <div>
@@ -94,107 +92,123 @@ export default function HeartbeatSettings() {
</div> </div>
<Separator className="my-4" /> <Separator className="my-4" />
{isLoading ? ( {status?.enabled ? (
<div className="flex items-center gap-2 text-muted-foreground py-4"> <EnabledState status={status} isTesting={isTesting} sendTestHeartbeat={sendTestHeartbeat} />
<LoaderCircleIcon className="h-4 w-4 animate-spin" />
<Trans>Loading...</Trans>
</div>
) : status?.enabled ? (
<div className="space-y-5">
<div className="flex items-center gap-2">
<Badge variant="success">
<Trans>Active</Trans>
</Badge>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<ConfigItem label={t`Endpoint URL`} value={status.url ?? ""} mono />
<ConfigItem label={t`Interval`} value={`${status.interval}s`} />
<ConfigItem label={t`HTTP Method`} value={status.method ?? "POST"} />
</div>
<Separator />
<div>
<h4 className="text-base font-medium mb-1">
<Trans>Test heartbeat</Trans>
</h4>
<p className="text-sm text-muted-foreground leading-relaxed mb-3">
<Trans>Send a single heartbeat ping to verify your endpoint is working.</Trans>
</p>
<Button
type="button"
variant="outline"
className="flex items-center gap-1.5"
onClick={sendTestHeartbeat}
disabled={isTesting}
>
<TestIcon className={clsx("h-4 w-4", isTesting && "animate-spin")} />
<Trans>Send test heartbeat</Trans>
</Button>
</div>
<Separator />
<div>
<h4 className="text-base font-medium mb-2">
<Trans>Payload format</Trans>
</h4>
<p className="text-sm text-muted-foreground leading-relaxed mb-2">
<Trans>
When using POST, each heartbeat includes a JSON payload with system status summary, list of down
systems, and triggered alerts.
</Trans>
</p>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>
The overall status is <code className="bg-muted rounded-sm px-1 text-primary">ok</code> when all systems
are up, <code className="bg-muted rounded-sm px-1 text-primary">warn</code> when alerts are triggered,
and <code className="bg-muted rounded-sm px-1 text-primary">error</code> when any system is down.
</Trans>
</p>
</div>
</div>
) : ( ) : (
<div className="grid gap-4"> <NotEnabledState isLoading={isLoading} />
<div>
<p className="text-sm text-muted-foreground leading-relaxed mb-3">
<Trans>Set the following environment variables on your Beszel hub to enable heartbeat monitoring:</Trans>
</p>
<div className="grid gap-2.5">
<EnvVarItem
name="HEARTBEAT_URL"
description={t`Endpoint URL to ping (required)`}
example="https://uptime.betterstack.com/api/v1/heartbeat/xxxx"
/>
<EnvVarItem name="HEARTBEAT_INTERVAL" description={t`Seconds between pings (default: 60)`} example="60" />
<EnvVarItem
name="HEARTBEAT_METHOD"
description={t`HTTP method: POST, GET, or HEAD (default: POST)`}
example="POST"
/>
</div>
</div>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>After setting the environment variables, restart your Beszel hub for changes to take effect.</Trans>
</p>
</div>
)} )}
</div> </div>
) )
} }
function EnabledState({
status,
isTesting,
sendTestHeartbeat,
}: {
status: HeartbeatStatus
isTesting: boolean
sendTestHeartbeat: () => void
}) {
const TestIcon = isTesting ? LoaderCircleIcon : SendIcon
return (
<div className="space-y-5">
<div className="flex items-center gap-2">
<Badge variant="success">
<Trans>Active</Trans>
</Badge>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<ConfigItem label={t`Endpoint URL`} value={status.url ?? ""} mono />
<ConfigItem label={t`Interval`} value={`${status.interval}s`} />
<ConfigItem label={t`HTTP Method`} value={status.method ?? "POST"} />
</div>
<Separator />
<div>
<h4 className="text-base font-medium mb-1">
<Trans>Test heartbeat</Trans>
</h4>
<p className="text-sm text-muted-foreground leading-relaxed mb-3">
<Trans>Send a single heartbeat ping to verify your endpoint is working.</Trans>
</p>
<Button
type="button"
variant="outline"
className="flex items-center gap-1.5"
onClick={sendTestHeartbeat}
disabled={isTesting}
>
<TestIcon className={cn("size-4", isTesting && "animate-spin")} />
<Trans>Send test heartbeat</Trans>
</Button>
</div>
<Separator />
<div>
<h4 className="text-base font-medium mb-2">
<Trans>Payload format</Trans>
</h4>
<p className="text-sm text-muted-foreground leading-relaxed mb-2">
<Trans>
When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems,
and triggered alerts.
</Trans>
</p>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>
The overall status is <code className="bg-muted rounded-sm px-1 text-primary">ok</code> when all systems are
up, <code className="bg-muted rounded-sm px-1 text-primary">warn</code> when alerts are triggered, and{" "}
<code className="bg-muted rounded-sm px-1 text-primary">error</code> when any system is down.
</Trans>
</p>
</div>
</div>
)
}
function NotEnabledState({ isLoading }: { isLoading?: boolean }) {
return (
<div className={cn("grid gap-4", isLoading && "animate-pulse")}>
<div>
<p className="text-sm text-muted-foreground leading-relaxed mb-3">
<Trans>Set the following environment variables on your Beszel hub to enable heartbeat monitoring:</Trans>
</p>
<div className="grid gap-2.5">
<EnvVarItem
name="HEARTBEAT_URL"
description={t`Endpoint URL to ping (required)`}
example="https://uptime.betterstack.com/api/v1/heartbeat/xxxx"
/>
<EnvVarItem name="HEARTBEAT_INTERVAL" description={t`Seconds between pings (default: 60)`} example="60" />
<EnvVarItem
name="HEARTBEAT_METHOD"
description={t`HTTP method: POST, GET, or HEAD (default: POST)`}
example="POST"
/>
</div>
</div>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>After setting the environment variables, restart your Beszel hub for changes to take effect.</Trans>
</p>
</div>
)
}
function ConfigItem({ label, value, mono }: { label: string; value: string; mono?: boolean }) { function ConfigItem({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
return ( return (
<div> <div>
<p className="text-sm font-medium mb-0.5">{label}</p> <p className="text-sm font-medium mb-0.5">{label}</p>
<p className={clsx("text-sm text-muted-foreground break-all", mono && "font-mono")}>{value}</p> <p className={cn("text-sm text-muted-foreground break-all", mono && "font-mono")}>{value}</p>
</div> </div>
) )
} }
function EnvVarItem({ name, description, example }: { name: string; description: string; example: string }) { function EnvVarItem({ name, description, example }: { name: string; description: string; example: string }) {
return ( return (
<div className="bg-muted/50 rounded-md px-3 py-2 grid gap-1.5"> <div className="bg-muted/50 rounded-md px-3 py-2.5 grid gap-1.5">
<code className="text-sm font-mono text-primary font-medium leading-tight">{name}</code> <code className="text-sm font-mono text-primary font-medium leading-tight">{name}</code>
<p className="text-sm text-muted-foreground">{description}</p> <p className="text-sm text-muted-foreground">{description}</p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">

View File

@@ -937,7 +937,6 @@ msgstr "متوسط التحميل"
msgid "Load state" msgid "Load state"
msgstr "حالة التحميل" msgstr "حالة التحميل"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "جاري التحميل..." msgstr "جاري التحميل..."

View File

@@ -937,7 +937,6 @@ msgstr "Средно натоварване"
msgid "Load state" msgid "Load state"
msgstr "Състояние на зареждане" msgstr "Състояние на зареждане"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Зареждане..." msgstr "Зареждане..."

View File

@@ -937,7 +937,6 @@ msgstr "Prům. zatížení"
msgid "Load state" msgid "Load state"
msgstr "Stav načtení" msgstr "Stav načtení"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Načítání..." msgstr "Načítání..."

View File

@@ -937,7 +937,6 @@ msgstr "Belastning gns."
msgid "Load state" msgid "Load state"
msgstr "Indlæsningstilstand" msgstr "Indlæsningstilstand"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Indlæser..." msgstr "Indlæser..."

View File

@@ -937,7 +937,6 @@ msgstr "Systemlast"
msgid "Load state" msgid "Load state"
msgstr "Ladezustand" msgstr "Ladezustand"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Lädt..." msgstr "Lädt..."

View File

@@ -932,7 +932,6 @@ msgstr "Load Avg"
msgid "Load state" msgid "Load state"
msgstr "Load state" msgstr "Load state"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Loading..." msgstr "Loading..."

View File

@@ -937,7 +937,6 @@ msgstr "Carga media"
msgid "Load state" msgid "Load state"
msgstr "Estado de carga" msgstr "Estado de carga"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Cargando..." msgstr "Cargando..."

View File

@@ -937,7 +937,6 @@ msgstr "میانگین بار"
msgid "Load state" msgid "Load state"
msgstr "وضعیت بارگذاری" msgstr "وضعیت بارگذاری"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "در حال بارگذاری..." msgstr "در حال بارگذاری..."

View File

@@ -937,7 +937,6 @@ msgstr "Charge moy."
msgid "Load state" msgid "Load state"
msgstr "État de charge" msgstr "État de charge"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Chargement..." msgstr "Chargement..."

View File

@@ -937,7 +937,6 @@ msgstr "ממוצע עומס"
msgid "Load state" msgid "Load state"
msgstr "מצב עומס" msgstr "מצב עומס"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "טוען..." msgstr "טוען..."

View File

@@ -937,7 +937,6 @@ msgstr "Prosječno Opterećenje"
msgid "Load state" msgid "Load state"
msgstr "Stanje učitavanja" msgstr "Stanje učitavanja"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Učitavanje..." msgstr "Učitavanje..."

View File

@@ -937,7 +937,6 @@ msgstr "Terhelési átlag"
msgid "Load state" msgid "Load state"
msgstr "Betöltési állapot" msgstr "Betöltési állapot"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Betöltés..." msgstr "Betöltés..."

View File

@@ -937,7 +937,6 @@ msgstr "Rata-rata Beban"
msgid "Load state" msgid "Load state"
msgstr "Beban saat ini" msgstr "Beban saat ini"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Memuat..." msgstr "Memuat..."

View File

@@ -937,7 +937,6 @@ msgstr "Carico Medio"
msgid "Load state" msgid "Load state"
msgstr "Stato di caricamento" msgstr "Stato di caricamento"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Caricamento..." msgstr "Caricamento..."

View File

@@ -937,7 +937,6 @@ msgstr "負荷平均"
msgid "Load state" msgid "Load state"
msgstr "ロード状態" msgstr "ロード状態"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "読み込み中..." msgstr "読み込み中..."

View File

@@ -937,7 +937,6 @@ msgstr "부하 평균"
msgid "Load state" msgid "Load state"
msgstr "로드 상태" msgstr "로드 상태"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "로딩 중..." msgstr "로딩 중..."

View File

@@ -937,7 +937,6 @@ msgstr "Gem. Belasting"
msgid "Load state" msgid "Load state"
msgstr "Laadstatus" msgstr "Laadstatus"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Laden..." msgstr "Laden..."

View File

@@ -937,7 +937,6 @@ msgstr "Snittbelastning"
msgid "Load state" msgid "Load state"
msgstr "Lastetilstand" msgstr "Lastetilstand"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Laster..." msgstr "Laster..."

View File

@@ -937,7 +937,6 @@ msgstr "Śr. obciążenie"
msgid "Load state" msgid "Load state"
msgstr "Stan obciążenia" msgstr "Stan obciążenia"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Ładowanie..." msgstr "Ładowanie..."

View File

@@ -937,7 +937,6 @@ msgstr "Carga Média"
msgid "Load state" msgid "Load state"
msgstr "Estado de carga" msgstr "Estado de carga"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Carregando..." msgstr "Carregando..."

View File

@@ -937,7 +937,6 @@ msgstr "Ср. загрузка"
msgid "Load state" msgid "Load state"
msgstr "Состояние загрузки" msgstr "Состояние загрузки"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Загрузка..." msgstr "Загрузка..."

View File

@@ -937,7 +937,6 @@ msgstr "Povpr. obrem."
msgid "Load state" msgid "Load state"
msgstr "Stanje nalaganja" msgstr "Stanje nalaganja"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Nalaganje..." msgstr "Nalaganje..."

View File

@@ -937,7 +937,6 @@ msgstr "Просечно опт."
msgid "Load state" msgid "Load state"
msgstr "Стање учитавања" msgstr "Стање учитавања"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Учитавање..." msgstr "Учитавање..."

View File

@@ -937,7 +937,6 @@ msgstr "Belastning"
msgid "Load state" msgid "Load state"
msgstr "Laddningstillstånd" msgstr "Laddningstillstånd"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Laddar..." msgstr "Laddar..."

View File

@@ -937,7 +937,6 @@ msgstr "Yük Ort."
msgid "Load state" msgid "Load state"
msgstr "Yükleme durumu" msgstr "Yükleme durumu"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Yükleniyor..." msgstr "Yükleniyor..."

View File

@@ -937,7 +937,6 @@ msgstr "Сер. навантаження"
msgid "Load state" msgid "Load state"
msgstr "Стан завантаження" msgstr "Стан завантаження"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Завантаження..." msgstr "Завантаження..."

View File

@@ -937,7 +937,6 @@ msgstr "Tải TB"
msgid "Load state" msgid "Load state"
msgstr "Trạng thái tải" msgstr "Trạng thái tải"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "Đang tải..." msgstr "Đang tải..."

View File

@@ -937,7 +937,6 @@ msgstr "负载"
msgid "Load state" msgid "Load state"
msgstr "加载状态" msgstr "加载状态"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "加载中..." msgstr "加载中..."

View File

@@ -937,7 +937,6 @@ msgstr "平均負載"
msgid "Load state" msgid "Load state"
msgstr "載入狀態" msgstr "載入狀態"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "載入中..." msgstr "載入中..."

View File

@@ -937,7 +937,6 @@ msgstr "平均負載"
msgid "Load state" msgid "Load state"
msgstr "載入狀態" msgstr "載入狀態"
#: src/components/routes/settings/heartbeat.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Loading..." msgid "Loading..."
msgstr "載入中..." msgstr "載入中..."