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 { getColor } from '@/util/color'
import { Box, Tooltip, Modal } from '@mantine/core' import { Box, Tooltip, Modal } from '@mantine/core'
import { useResizeObserver } from '@mantine/hooks' import { useResizeObserver } from '@mantine/hooks'

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { Text, Tooltip } from '@mantine/core' 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 { IconAlertCircle, IconAlertTriangle, IconCircleCheck } from '@tabler/icons-react'
import DetailChart from './DetailChart' import DetailChart from './DetailChart'
import DetailBar from './DetailBar' import DetailBar from './DetailBar'
@ -39,8 +39,8 @@ export default function MonitorDetail({
// Hide real status icon if monitor is in maintenance // Hide real status icon if monitor is in maintenance
const now = new Date() const now = new Date()
const hasMaintenance = (maintenances || []) const hasMaintenance = maintenances
.filter((m) => (!m.start && !m.end) || (m.start && m.end && now >= m.start && now <= m.end)) .filter((m) => now >= new Date(m.start) && (!m.end || now <= new Date(m.end)))
.find((maintenance) => maintenance.monitors?.includes(monitor.id)) .find((maintenance) => maintenance.monitors?.includes(monitor.id))
if (hasMaintenance) if (hasMaintenance)
statusIcon = ( 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 { Accordion, Card, Center, Text } from '@mantine/core'
import MonitorDetail from './MonitorDetail' import MonitorDetail from './MonitorDetail'
import { pageConfig } from '@/uptime.config' 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 { Center, Container, Title } from '@mantine/core'
import { IconCircleCheck, IconAlertCircle } from '@tabler/icons-react' import { IconCircleCheck, IconAlertCircle } from '@tabler/icons-react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -21,8 +21,8 @@ export default function OverallStatus({
monitors, monitors,
}: { }: {
state: { overallUp: number; overallDown: number; lastUpdate: number } state: { overallUp: number; overallDown: number; lastUpdate: number }
maintenances: Maintenances[] maintenances: MaintenanceConfig[]
monitors: Monitor[] monitors: MonitorTarget[]
}) { }) {
let group = pageConfig.group let group = pageConfig.group
let groupedMonitor = (group && Object.keys(group).length > 0) || false let groupedMonitor = (group && Object.keys(group).length > 0) || false
@ -58,10 +58,9 @@ export default function OverallStatus({
}) })
const now = new Date() const now = new Date()
let filteredMaintenances: (Omit<Maintenances, 'monitors'> & { let filteredMaintenances: (Omit<MaintenanceConfig, 'monitors'> & { monitors: MonitorTarget[] })[] =
monitors: Monitor[] | undefined maintenances
})[] = (maintenances || []) .filter((m) => now >= new Date(m.start) && (!m.end || now <= new Date(m.end)))
.filter((m) => (!m.start && !m.end) || (m.start && m.end && now >= m.start && now <= m.end))
.map((maintenance) => ({ .map((maintenance) => ({
...maintenance, ...maintenance,
monitors: maintenance.monitors?.map( monitors: maintenance.monitors?.map(
@ -83,7 +82,11 @@ export default function OverallStatus({
</Title> </Title>
{filteredMaintenances.map((maintenance, idx) => ( {filteredMaintenances.map((maintenance, idx) => (
<MaintenanceAlert groupedMonitor={groupedMonitor} key={idx} maintenance={maintenance} /> <MaintenanceAlert
key={idx}
maintenance={maintenance}
style={{ maxWidth: groupedMonitor ? '897px' : '865px' }}
/>
))} ))}
</Container> </Container>
) )

View File

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

View File

@ -1,7 +1,7 @@
import Head from 'next/head' import Head from 'next/head'
import { Inter } from 'next/font/google' 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 { KVNamespace } from '@cloudflare/workers-types'
import { maintenances, pageConfig, workerConfig } from '@/uptime.config' import { maintenances, pageConfig, workerConfig } from '@/uptime.config'
import OverallStatus from '@/components/OverallStatus' import OverallStatus from '@/components/OverallStatus'

View File

@ -4,18 +4,12 @@ export type PageConfig = {
group?: PageConfigGroup group?: PageConfigGroup
} }
export type Maintenances = { export type MaintenanceConfig = {
// title to display monitors: string[]
title: string title?: string
// array of monitor ids body: string
monitors?: string[] start: number | string
// body message end?: number | string
body?: string
// start date
start?: Date
// end Date
end?: Date
// display color
color?: string color?: string
} }
@ -27,17 +21,10 @@ export type PageConfigLink = {
highlight?: boolean highlight?: boolean
} }
export type WorkerConfig = { export type MonitorTarget = {
kvWriteCooldownMinutes: number
monitors: Monitor[]
notification?: Notification
callbacks?: Callbacks
}
export type Monitor = {
id: string id: string
name: string name: string
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'TCP_PING' method: string
target: string target: string
tooltip?: string tooltip?: string
statusPageLink?: string statusPageLink?: string
@ -52,6 +39,13 @@ export type Monitor = {
checkProxyFallback?: boolean checkProxyFallback?: boolean
} }
export type WorkerConfig = {
kvWriteCooldownMinutes: number
monitors: MonitorTarget[]
notification?: Notification
callbacks?: Callbacks
}
export type Notification = { export type Notification = {
appriseApiServer?: string appriseApiServer?: string
recipientUrl?: string recipientUrl?: string
@ -77,3 +71,33 @@ export type Callbacks = {
reason: string reason: string
) => Promise<any> ) => 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 { workerConfig, maintenances } from '../../uptime.config'
import { formatStatusChangeNotification, getWorkerLocation, notifyWithApprise } from './util' import { formatStatusChangeNotification, getWorkerLocation, notifyWithApprise } from './util'
import { MonitorState, MonitorTarget } from '../../types/uptime.types' import { MonitorState, MonitorTarget } from '../../types/config'
import { getStatus } from './monitor' import { getStatus } from './monitor'
import { DurableObject } from 'cloudflare:workers' import { DurableObject } from 'cloudflare:workers'
@ -22,23 +22,30 @@ const Worker = {
timeNow: number, timeNow: number,
reason: string 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 // 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( console.log(
`Skipping notification for ${monitor.name} (${monitor.id} in skipNotificationIds)` `Skipping notification for ${monitor.name} (${monitor.id} in skipNotificationIds)`
) )
return 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) { if (workerConfig.notification?.appriseApiServer && workerConfig.notification?.recipientUrl) {
const notification = formatStatusChangeNotification( const notification = formatStatusChangeNotification(
monitor, monitor,