alert config/style changes, worker code improve

pull/101/head
lyc8503 2025-04-27 01:12:13 +08:00
parent 1dc02feeca
commit 2acff525e9
11 changed files with 113 additions and 129 deletions

View File

@ -1,4 +1,4 @@
import { MonitorState, MonitorTarget } from '@/types/uptime.types'
import { MonitorState, MonitorTarget } from '@/types/config'
import { getColor } from '@/util/color'
import { Box, Tooltip, Modal } from '@mantine/core'
import { useResizeObserver } from '@mantine/hooks'

View File

@ -11,7 +11,7 @@ import {
TimeScale,
} from 'chart.js'
import 'chartjs-adapter-moment'
import { MonitorState, MonitorTarget } from '@/types/uptime.types'
import { MonitorState, MonitorTarget } from '@/types/config'
import { iataToCountry } from '@/util/iata'
ChartJS.register(

View File

@ -1,55 +1,42 @@
import { Alert, List, Text } from '@mantine/core'
import { IconAlertTriangle } from '@tabler/icons-react'
import { Maintenances, Monitor } from '@/types/config'
import { pageConfig } from '@/uptime.config'
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
export default function MaintenanceAlert({
maintenance,
groupedMonitor,
style,
}: {
maintenance: Omit<Maintenances, 'monitors'> & { monitors: Monitor[] | undefined }
groupedMonitor: boolean
maintenance: Omit<MaintenanceConfig, 'monitors'> & { monitors: MonitorTarget[] }
style?: React.CSSProperties
}) {
return (
<Alert
icon={<IconAlertTriangle />}
title={maintenance.title}
color={maintenance.color || 'primary'}
mt="md"
ml="md"
mr="md"
title={maintenance.title || 'Scheduled Maintenance'}
color={maintenance.color || 'yellow'}
withCloseButton={false}
style={{ maxWidth: groupedMonitor ? '897px' : '865px', position: 'relative' }}
style={{ position: 'relative', margin: '16px auto 0 auto', ...style }}
>
{/* Date range in top right */}
{(maintenance.start || maintenance.end) && (
<div
style={{
position: 'absolute',
top: 10,
right: 10,
fontSize: '0.85rem',
textAlign: 'right',
padding: '2px 8px',
borderRadius: 6,
}}
>
{maintenance.start && (
<>
<b>From:</b> {maintenance.start.toLocaleString()}
<br />
</>
)}
{maintenance.end && (
<>
<b>To:</b> {maintenance.end.toLocaleString()}
</>
)}
</div>
)}
<div
style={{
position: 'absolute',
top: 10,
right: 10,
fontSize: '0.85rem',
textAlign: 'right',
padding: '2px 8px',
borderRadius: 6,
}}
>
<b>From:</b> {new Date(maintenance.start).toLocaleString()}
<br />
<b>To:</b>{' '}
{maintenance.end ? new Date(maintenance.end).toLocaleString() : 'Until further notice'}
</div>
{maintenance.body && <Text mt="xs">{maintenance.body}</Text>}
{maintenance.monitors && maintenance.monitors.length > 0 && (
<Text>{maintenance.body}</Text>
{maintenance.monitors.length > 0 && (
<>
<Text mt="xs">
<b>Affected components:</b>

View File

@ -1,5 +1,5 @@
import { Text, Tooltip } from '@mantine/core'
import { MonitorState, MonitorTarget } from '@/types/uptime.types'
import { MonitorState, MonitorTarget } from '@/types/config'
import { IconAlertCircle, IconAlertTriangle, IconCircleCheck } from '@tabler/icons-react'
import DetailChart from './DetailChart'
import DetailBar from './DetailBar'
@ -39,8 +39,8 @@ export default function MonitorDetail({
// Hide real status icon if monitor is in maintenance
const now = new Date()
const hasMaintenance = (maintenances || [])
.filter((m) => (!m.start && !m.end) || (m.start && m.end && now >= m.start && now <= m.end))
const hasMaintenance = maintenances
.filter((m) => now >= new Date(m.start) && (!m.end || now <= new Date(m.end)))
.find((maintenance) => maintenance.monitors?.includes(monitor.id))
if (hasMaintenance)
statusIcon = (

View File

@ -1,4 +1,4 @@
import { MonitorState, MonitorTarget } from '@/types/uptime.types'
import { MonitorState, MonitorTarget } from '@/types/config'
import { Accordion, Card, Center, Text } from '@mantine/core'
import MonitorDetail from './MonitorDetail'
import { pageConfig } from '@/uptime.config'

View File

@ -1,4 +1,4 @@
import { Maintenances, Monitor } from '@/types/config'
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
import { Center, Container, Title } from '@mantine/core'
import { IconCircleCheck, IconAlertCircle } from '@tabler/icons-react'
import { useEffect, useState } from 'react'
@ -21,8 +21,8 @@ export default function OverallStatus({
monitors,
}: {
state: { overallUp: number; overallDown: number; lastUpdate: number }
maintenances: Maintenances[]
monitors: Monitor[]
maintenances: MaintenanceConfig[]
monitors: MonitorTarget[]
}) {
let group = pageConfig.group
let groupedMonitor = (group && Object.keys(group).length > 0) || false
@ -58,16 +58,15 @@ export default function OverallStatus({
})
const now = new Date()
let filteredMaintenances: (Omit<Maintenances, 'monitors'> & {
monitors: Monitor[] | undefined
})[] = (maintenances || [])
.filter((m) => (!m.start && !m.end) || (m.start && m.end && now >= m.start && now <= m.end))
.map((maintenance) => ({
...maintenance,
monitors: maintenance.monitors?.map(
(monitorId) => monitors.find((mon) => monitorId === mon.id)!
),
}))
let filteredMaintenances: (Omit<MaintenanceConfig, 'monitors'> & { monitors: MonitorTarget[] })[] =
maintenances
.filter((m) => now >= new Date(m.start) && (!m.end || now <= new Date(m.end)))
.map((maintenance) => ({
...maintenance,
monitors: maintenance.monitors?.map(
(monitorId) => monitors.find((mon) => monitorId === mon.id)!
),
}))
return (
<Container size="md" mt="xl">
@ -83,7 +82,11 @@ export default function OverallStatus({
</Title>
{filteredMaintenances.map((maintenance, idx) => (
<MaintenanceAlert groupedMonitor={groupedMonitor} key={idx} maintenance={maintenance} />
<MaintenanceAlert
key={idx}
maintenance={maintenance}
style={{ maxWidth: groupedMonitor ? '897px' : '865px' }}
/>
))}
</Container>
)

View File

@ -1,5 +1,5 @@
import { workerConfig } from '@/uptime.config'
import { MonitorState } from '@/types/uptime.types'
import { MonitorState } from '@/types/config'
import { NextRequest } from 'next/server'
export const runtime = 'edge'

View File

@ -1,7 +1,7 @@
import Head from 'next/head'
import { Inter } from 'next/font/google'
import { MonitorState, MonitorTarget } from '@/types/uptime.types'
import { MonitorState, MonitorTarget } from '@/types/config'
import { KVNamespace } from '@cloudflare/workers-types'
import { maintenances, pageConfig, workerConfig } from '@/uptime.config'
import OverallStatus from '@/components/OverallStatus'

View File

@ -4,18 +4,12 @@ export type PageConfig = {
group?: PageConfigGroup
}
export type Maintenances = {
// title to display
title: string
// array of monitor ids
monitors?: string[]
// body message
body?: string
// start date
start?: Date
// end Date
end?: Date
// display color
export type MaintenanceConfig = {
monitors: string[]
title?: string
body: string
start: number | string
end?: number | string
color?: string
}
@ -27,17 +21,10 @@ export type PageConfigLink = {
highlight?: boolean
}
export type WorkerConfig = {
kvWriteCooldownMinutes: number
monitors: Monitor[]
notification?: Notification
callbacks?: Callbacks
}
export type Monitor = {
export type MonitorTarget = {
id: string
name: string
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'TCP_PING'
method: string
target: string
tooltip?: string
statusPageLink?: string
@ -52,6 +39,13 @@ export type Monitor = {
checkProxyFallback?: boolean
}
export type WorkerConfig = {
kvWriteCooldownMinutes: number
monitors: MonitorTarget[]
notification?: Notification
callbacks?: Callbacks
}
export type Notification = {
appriseApiServer?: string
recipientUrl?: string
@ -77,3 +71,33 @@ export type Callbacks = {
reason: string
) => Promise<any>
}
export type MonitorState = {
lastUpdate: number
overallUp: number
overallDown: number
incident: Record<
string,
{
start: number[]
end: number | undefined // undefined if it's still open
error: string[]
}[]
>
latency: Record<
string,
{
recent: {
loc: string
ping: number
time: number
}[] // recent 12 hour data, 2 min interval
all: {
loc: string
ping: number
time: number
}[] // all data in 90 days, 1 hour interval
}
>
}

View File

@ -1,37 +0,0 @@
import { Monitor } from './config'
type MonitorState = {
lastUpdate: number
overallUp: number
overallDown: number
incident: Record<
string,
{
start: number[]
end: number | undefined // undefined if it's still open
error: string[]
}[]
>
latency: Record<
string,
{
recent: {
loc: string
ping: number
time: number
}[] // recent 12 hour data, 2 min interval
all: {
loc: string
ping: number
time: number
}[] // all data in 90 days, 1 hour interval
}
>
}
type MonitorTarget = Monitor & {
body?: BodyInit
}
export type { MonitorState, MonitorTarget }

View File

@ -1,6 +1,6 @@
import { workerConfig, maintenances } from '../../uptime.config'
import { formatStatusChangeNotification, getWorkerLocation, notifyWithApprise } from './util'
import { MonitorState, MonitorTarget } from '../../types/uptime.types'
import { MonitorState, MonitorTarget } from '../../types/config'
import { getStatus } from './monitor'
import { DurableObject } from 'cloudflare:workers'
@ -22,23 +22,30 @@ const Worker = {
timeNow: number,
reason: string
) => {
const skipList: Set<string> = new Set(workerConfig.notification?.skipNotificationIds)
const now = new Date()
// build list of ids that are currently in maintenance (based on start & end OR if they are missing) and merge with the skipList setting
maintenances
.filter((m) => (!m.start && !m.end) || (m.start && m.end && now >= m.start && now <= m.end))
.map((e) => e.monitors || [])
.flat()
.map((monitor) => skipList.add(monitor))
// Skip notification if monitor is in the skip list
if (skipList && skipList.has(monitor.id)) {
const skipList = workerConfig.notification?.skipNotificationIds
if (skipList && skipList.includes(monitor.id)) {
console.log(
`Skipping notification for ${monitor.name} (${monitor.id} in skipNotificationIds)`
)
return
}
// Skip notification if monitor is in maintenance
const maintenanceList = maintenances
.filter(
(m) =>
new Date(timeNow * 1000) >= new Date(m.start) &&
(!m.end || new Date(timeNow * 1000) <= new Date(m.end))
)
.map((e) => e.monitors)
.flat()
if (maintenanceList.includes(monitor.id)) {
console.log(`Skipping notification for ${monitor.name} (in maintenance)`)
return
}
if (workerConfig.notification?.appriseApiServer && workerConfig.notification?.recipientUrl) {
const notification = formatStatusChangeNotification(
monitor,