11import { t } from "@lingui/core/macro"
22import { Trans } from "@lingui/react/macro"
33import { redirectPage } from "@nanostores/router"
4- import clsx from "clsx"
54import { LoaderCircleIcon , SendIcon } from "lucide-react"
65import { useEffect , useState } from "react"
76import { $router } from "@/components/router"
@@ -10,6 +9,7 @@ import { Button } from "@/components/ui/button"
109import { Separator } from "@/components/ui/separator"
1110import { toast } from "@/components/ui/use-toast"
1211import { isAdmin , pb } from "@/lib/api"
12+ import { cn } from "@/lib/utils"
1313
1414interface HeartbeatStatus {
1515 enabled : boolean
@@ -37,10 +37,10 @@ export default function HeartbeatSettings() {
3737 setIsLoading ( true )
3838 const res = await pb . send < HeartbeatStatus > ( "/api/beszel/heartbeat-status" , { } )
3939 setStatus ( res )
40- } catch ( error : any ) {
40+ } catch ( error : unknown ) {
4141 toast ( {
4242 title : t `Error` ,
43- description : error . message ,
43+ description : ( error as Error ) . message ,
4444 variant : "destructive" ,
4545 } )
4646 } finally {
@@ -66,19 +66,17 @@ export default function HeartbeatSettings() {
6666 variant : "destructive" ,
6767 } )
6868 }
69- } catch ( error : any ) {
69+ } catch ( error : unknown ) {
7070 toast ( {
7171 title : t `Error` ,
72- description : error . message ,
72+ description : ( error as Error ) . message ,
7373 variant : "destructive" ,
7474 } )
7575 } finally {
7676 setIsTesting ( false )
7777 }
7878 }
7979
80- const TestIcon = isTesting ? LoaderCircleIcon : SendIcon
81-
8280 return (
8381 < div >
8482 < div >
@@ -94,107 +92,123 @@ export default function HeartbeatSettings() {
9492 </ div >
9593 < Separator className = "my-4" />
9694
97- { isLoading ? (
98- < div className = "flex items-center gap-2 text-muted-foreground py-4" >
99- < LoaderCircleIcon className = "h-4 w-4 animate-spin" />
100- < Trans > Loading...</ Trans >
101- </ div >
102- ) : status ?. enabled ? (
103- < div className = "space-y-5" >
104- < div className = "flex items-center gap-2" >
105- < Badge variant = "success" >
106- < Trans > Active</ Trans >
107- </ Badge >
108- </ div >
109- < div className = "grid gap-4 sm:grid-cols-2" >
110- < ConfigItem label = { t `Endpoint URL` } value = { status . url ?? "" } mono />
111- < ConfigItem label = { t `Interval` } value = { `${ status . interval } s` } />
112- < ConfigItem label = { t `HTTP Method` } value = { status . method ?? "POST" } />
113- </ div >
114-
115- < Separator />
116-
117- < div >
118- < h4 className = "text-base font-medium mb-1" >
119- < Trans > Test heartbeat</ Trans >
120- </ h4 >
121- < p className = "text-sm text-muted-foreground leading-relaxed mb-3" >
122- < Trans > Send a single heartbeat ping to verify your endpoint is working.</ Trans >
123- </ p >
124- < Button
125- type = "button"
126- variant = "outline"
127- className = "flex items-center gap-1.5"
128- onClick = { sendTestHeartbeat }
129- disabled = { isTesting }
130- >
131- < TestIcon className = { clsx ( "h-4 w-4" , isTesting && "animate-spin" ) } />
132- < Trans > Send test heartbeat</ Trans >
133- </ Button >
134- </ div >
135-
136- < Separator />
137-
138- < div >
139- < h4 className = "text-base font-medium mb-2" >
140- < Trans > Payload format</ Trans >
141- </ h4 >
142- < p className = "text-sm text-muted-foreground leading-relaxed mb-2" >
143- < Trans >
144- When using POST, each heartbeat includes a JSON payload with system status summary, list of down
145- systems, and triggered alerts.
146- </ Trans >
147- </ p >
148- < p className = "text-sm text-muted-foreground leading-relaxed" >
149- < Trans >
150- The overall status is < code className = "bg-muted rounded-sm px-1 text-primary" > ok</ code > when all systems
151- are up, < code className = "bg-muted rounded-sm px-1 text-primary" > warn</ code > when alerts are triggered,
152- and < code className = "bg-muted rounded-sm px-1 text-primary" > error</ code > when any system is down.
153- </ Trans >
154- </ p >
155- </ div >
156- </ div >
95+ { status ?. enabled ? (
96+ < EnabledState status = { status } isTesting = { isTesting } sendTestHeartbeat = { sendTestHeartbeat } />
15797 ) : (
158- < div className = "grid gap-4" >
159- < div >
160- < p className = "text-sm text-muted-foreground leading-relaxed mb-3" >
161- < Trans > Set the following environment variables on your Beszel hub to enable heartbeat monitoring:</ Trans >
162- </ p >
163- < div className = "grid gap-2.5" >
164- < EnvVarItem
165- name = "HEARTBEAT_URL"
166- description = { t `Endpoint URL to ping (required)` }
167- example = "https://uptime.betterstack.com/api/v1/heartbeat/xxxx"
168- />
169- < EnvVarItem name = "HEARTBEAT_INTERVAL" description = { t `Seconds between pings (default: 60)` } example = "60" />
170- < EnvVarItem
171- name = "HEARTBEAT_METHOD"
172- description = { t `HTTP method: POST, GET, or HEAD (default: POST)` }
173- example = "POST"
174- />
175- </ div >
176- </ div >
177- < p className = "text-sm text-muted-foreground leading-relaxed" >
178- < Trans > After setting the environment variables, restart your Beszel hub for changes to take effect.</ Trans >
179- </ p >
180- </ div >
98+ < NotEnabledState isLoading = { isLoading } />
18199 ) }
182100 </ div >
183101 )
184102}
185103
104+ function EnabledState ( {
105+ status,
106+ isTesting,
107+ sendTestHeartbeat,
108+ } : {
109+ status : HeartbeatStatus
110+ isTesting : boolean
111+ sendTestHeartbeat : ( ) => void
112+ } ) {
113+ const TestIcon = isTesting ? LoaderCircleIcon : SendIcon
114+ return (
115+ < div className = "space-y-5" >
116+ < div className = "flex items-center gap-2" >
117+ < Badge variant = "success" >
118+ < Trans > Active</ Trans >
119+ </ Badge >
120+ </ div >
121+ < div className = "grid gap-4 sm:grid-cols-2" >
122+ < ConfigItem label = { t `Endpoint URL` } value = { status . url ?? "" } mono />
123+ < ConfigItem label = { t `Interval` } value = { `${ status . interval } s` } />
124+ < ConfigItem label = { t `HTTP Method` } value = { status . method ?? "POST" } />
125+ </ div >
126+
127+ < Separator />
128+
129+ < div >
130+ < h4 className = "text-base font-medium mb-1" >
131+ < Trans > Test heartbeat</ Trans >
132+ </ h4 >
133+ < p className = "text-sm text-muted-foreground leading-relaxed mb-3" >
134+ < Trans > Send a single heartbeat ping to verify your endpoint is working.</ Trans >
135+ </ p >
136+ < Button
137+ type = "button"
138+ variant = "outline"
139+ className = "flex items-center gap-1.5"
140+ onClick = { sendTestHeartbeat }
141+ disabled = { isTesting }
142+ >
143+ < TestIcon className = { cn ( "size-4" , isTesting && "animate-spin" ) } />
144+ < Trans > Send test heartbeat</ Trans >
145+ </ Button >
146+ </ div >
147+
148+ < Separator />
149+
150+ < div >
151+ < h4 className = "text-base font-medium mb-2" >
152+ < Trans > Payload format</ Trans >
153+ </ h4 >
154+ < p className = "text-sm text-muted-foreground leading-relaxed mb-2" >
155+ < Trans >
156+ When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems,
157+ and triggered alerts.
158+ </ Trans >
159+ </ p >
160+ < p className = "text-sm text-muted-foreground leading-relaxed" >
161+ < Trans >
162+ The overall status is < code className = "bg-muted rounded-sm px-1 text-primary" > ok</ code > when all systems are
163+ up, < code className = "bg-muted rounded-sm px-1 text-primary" > warn</ code > when alerts are triggered, and{ " " }
164+ < code className = "bg-muted rounded-sm px-1 text-primary" > error</ code > when any system is down.
165+ </ Trans >
166+ </ p >
167+ </ div >
168+ </ div >
169+ )
170+ }
171+
172+ function NotEnabledState ( { isLoading } : { isLoading ?: boolean } ) {
173+ return (
174+ < div className = { cn ( "grid gap-4" , isLoading && "animate-pulse" ) } >
175+ < div >
176+ < p className = "text-sm text-muted-foreground leading-relaxed mb-3" >
177+ < Trans > Set the following environment variables on your Beszel hub to enable heartbeat monitoring:</ Trans >
178+ </ p >
179+ < div className = "grid gap-2.5" >
180+ < EnvVarItem
181+ name = "HEARTBEAT_URL"
182+ description = { t `Endpoint URL to ping (required)` }
183+ example = "https://uptime.betterstack.com/api/v1/heartbeat/xxxx"
184+ />
185+ < EnvVarItem name = "HEARTBEAT_INTERVAL" description = { t `Seconds between pings (default: 60)` } example = "60" />
186+ < EnvVarItem
187+ name = "HEARTBEAT_METHOD"
188+ description = { t `HTTP method: POST, GET, or HEAD (default: POST)` }
189+ example = "POST"
190+ />
191+ </ div >
192+ </ div >
193+ < p className = "text-sm text-muted-foreground leading-relaxed" >
194+ < Trans > After setting the environment variables, restart your Beszel hub for changes to take effect.</ Trans >
195+ </ p >
196+ </ div >
197+ )
198+ }
199+
186200function ConfigItem ( { label, value, mono } : { label : string ; value : string ; mono ?: boolean } ) {
187201 return (
188202 < div >
189203 < p className = "text-sm font-medium mb-0.5" > { label } </ p >
190- < p className = { clsx ( "text-sm text-muted-foreground break-all" , mono && "font-mono" ) } > { value } </ p >
204+ < p className = { cn ( "text-sm text-muted-foreground break-all" , mono && "font-mono" ) } > { value } </ p >
191205 </ div >
192206 )
193207}
194208
195209function EnvVarItem ( { name, description, example } : { name : string ; description : string ; example : string } ) {
196210 return (
197- < div className = "bg-muted/50 rounded-md px-3 py-2 grid gap-1.5" >
211+ < div className = "bg-muted/50 rounded-md px-3 py-2.5 grid gap-1.5" >
198212 < code className = "text-sm font-mono text-primary font-medium leading-tight" > { name } </ code >
199213 < p className = "text-sm text-muted-foreground" > { description } </ p >
200214 < p className = "text-xs text-muted-foreground" >
0 commit comments