Merge branch 'main' of https://github.com/lyc8503/UptimeFlare
commit
8a097698be
|
|
@ -56,6 +56,8 @@ Please refer to [Wiki](https://github.com/lyc8503/UptimeFlare/wiki)
|
||||||
- [x] Groups
|
- [x] Groups
|
||||||
- [x] Remove old incidents
|
- [x] Remove old incidents
|
||||||
- [x] ~~Known issue~~: `fetch` doesn't support non-standard port (resolved after CF update)
|
- [x] ~~Known issue~~: `fetch` doesn't support non-standard port (resolved after CF update)
|
||||||
- [ ] Compatibility date update
|
- [x] Compatibility date update
|
||||||
- [ ] Update wiki and add docs for dev
|
- [x] Scheduled Maintenance
|
||||||
|
- [ ] Update wiki/README and add docs for dev
|
||||||
|
- [ ] Migration to Terraform Cloudflare provider version 5.x
|
||||||
- [ ] Cloudflare D1 database
|
- [ ] Cloudflare D1 database
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { MonitorState, MonitorTarget } from '@/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'
|
||||||
|
|
|
||||||
|
|
@ -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 '@/uptime.types'
|
import { MonitorState, MonitorTarget } from '@/types/config'
|
||||||
import { iataToCountry } from '@/util/iata'
|
import { iataToCountry } from '@/util/iata'
|
||||||
|
|
||||||
ChartJS.register(
|
ChartJS.register(
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { Container, Group, Text } from '@mantine/core'
|
import { Container, Group, Text } from '@mantine/core'
|
||||||
import classes from '@/styles/Header.module.css'
|
import classes from '@/styles/Header.module.css'
|
||||||
import { pageConfig } from '@/uptime.config'
|
import { pageConfig } from '@/uptime.config'
|
||||||
|
import { PageConfigLink } from '@/types/config'
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const linkToElement = (link: { label: string; link: string; highlight?: boolean }) => {
|
const linkToElement = (link: PageConfigLink) => {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
key={link.label}
|
key={link.label}
|
||||||
|
|
@ -38,11 +39,11 @@ export default function Header() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Group gap={5} visibleFrom="sm">
|
<Group gap={5} visibleFrom="sm">
|
||||||
{pageConfig.links.map(linkToElement)}
|
{pageConfig.links?.map(linkToElement)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group gap={5} hiddenFrom="sm">
|
<Group gap={5} hiddenFrom="sm">
|
||||||
{pageConfig.links.filter((link) => (link as any).highlight).map(linkToElement)}
|
{pageConfig.links?.filter((link) => link.highlight).map(linkToElement)}
|
||||||
</Group>
|
</Group>
|
||||||
</Container>
|
</Container>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Alert, List, Text } from '@mantine/core'
|
||||||
|
import { IconAlertTriangle } from '@tabler/icons-react'
|
||||||
|
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
|
||||||
|
|
||||||
|
export default function MaintenanceAlert({
|
||||||
|
maintenance,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
maintenance: Omit<MaintenanceConfig, 'monitors'> & { monitors?: MonitorTarget[] }
|
||||||
|
style?: React.CSSProperties
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
icon={<IconAlertTriangle />}
|
||||||
|
title={maintenance.title || 'Scheduled Maintenance'}
|
||||||
|
color={maintenance.color || 'yellow'}
|
||||||
|
withCloseButton={false}
|
||||||
|
style={{ position: 'relative', margin: '16px auto 0 auto', ...style }}
|
||||||
|
>
|
||||||
|
{/* Date range in top right */}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<Text>{maintenance.body}</Text>
|
||||||
|
{maintenance.monitors && maintenance.monitors.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Text mt="xs">
|
||||||
|
<b>Affected components:</b>
|
||||||
|
</Text>
|
||||||
|
<List size="sm" withPadding>
|
||||||
|
{maintenance.monitors.map((comp, compIdx) => (
|
||||||
|
<List.Item key={compIdx}>{comp.name}</List.Item>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Alert>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { Text, Tooltip } from '@mantine/core'
|
import { Text, Tooltip } from '@mantine/core'
|
||||||
import { MonitorState, MonitorTarget } from '@/uptime.types'
|
import { MonitorState, MonitorTarget } from '@/types/config'
|
||||||
import { IconAlertCircle, 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'
|
||||||
import { getColor } from '@/util/color'
|
import { getColor } from '@/util/color'
|
||||||
|
import { maintenances } from '@/uptime.config'
|
||||||
|
|
||||||
export default function MonitorDetail({
|
export default function MonitorDetail({
|
||||||
monitor,
|
monitor,
|
||||||
|
|
@ -25,11 +26,32 @@ export default function MonitorDetail({
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
const statusIcon =
|
let statusIcon =
|
||||||
state.incident[monitor.id].slice(-1)[0].end === undefined ? (
|
state.incident[monitor.id].slice(-1)[0].end === undefined ? (
|
||||||
<IconAlertCircle style={{ width: '1.25em', height: '1.25em', color: '#b91c1c' }} />
|
<IconAlertCircle
|
||||||
|
style={{ width: '1.25em', height: '1.25em', color: '#b91c1c', marginRight: '3px' }}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<IconCircleCheck style={{ width: '1.25em', height: '1.25em', color: '#059669' }} />
|
<IconCircleCheck
|
||||||
|
style={{ width: '1.25em', height: '1.25em', color: '#059669', marginRight: '3px' }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hide real status icon if monitor is in maintenance
|
||||||
|
const now = new Date()
|
||||||
|
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 = (
|
||||||
|
<IconAlertTriangle
|
||||||
|
style={{
|
||||||
|
width: '1.25em',
|
||||||
|
height: '1.25em',
|
||||||
|
color: '#fab005',
|
||||||
|
marginRight: '3px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
let totalTime = Date.now() / 1000 - state.incident[monitor.id][0].start[0]
|
let totalTime = Date.now() / 1000 - state.incident[monitor.id][0].start[0]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { MonitorState, MonitorTarget } from '@/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'
|
||||||
|
|
@ -102,8 +102,8 @@ export default function MonitorList({
|
||||||
shadow="sm"
|
shadow="sm"
|
||||||
padding="lg"
|
padding="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
ml="xl"
|
ml="md"
|
||||||
mr="xl"
|
mr="md"
|
||||||
mt="xl"
|
mt="xl"
|
||||||
withBorder={!groupedMonitor}
|
withBorder={!groupedMonitor}
|
||||||
style={{ width: groupedMonitor ? '897px' : '865px' }}
|
style={{ width: groupedMonitor ? '897px' : '865px' }}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,32 @@
|
||||||
import { Center, Title } from '@mantine/core'
|
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
|
||||||
|
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'
|
||||||
|
import MaintenanceAlert from './MaintenanceAlert'
|
||||||
|
import { pageConfig } from '@/uptime.config'
|
||||||
|
|
||||||
function useWindowVisibility() {
|
function useWindowVisibility() {
|
||||||
const [isVisible, setIsVisible] = useState(true)
|
const [isVisible, setIsVisible] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleVisibilityChange = () => {
|
const handleVisibilityChange = () => setIsVisible(document.visibilityState === 'visible')
|
||||||
console.log('visibility change', document.visibilityState)
|
|
||||||
setIsVisible(document.visibilityState === 'visible')
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||||
|
return () => document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||||
return () => {
|
|
||||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return isVisible
|
return isVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function OverallStatus({
|
export default function OverallStatus({
|
||||||
state,
|
state,
|
||||||
|
maintenances,
|
||||||
|
monitors,
|
||||||
}: {
|
}: {
|
||||||
state: { overallUp: number; overallDown: number; lastUpdate: number }
|
state: { overallUp: number; overallDown: number; lastUpdate: number }
|
||||||
|
maintenances: MaintenanceConfig[]
|
||||||
|
monitors: MonitorTarget[]
|
||||||
}) {
|
}) {
|
||||||
|
let group = pageConfig.group
|
||||||
|
let groupedMonitor = (group && Object.keys(group).length > 0) || false
|
||||||
|
|
||||||
let statusString = ''
|
let statusString = ''
|
||||||
let icon = <IconAlertCircle style={{ width: 64, height: 64, color: '#b91c1c' }} />
|
let icon = <IconAlertCircle style={{ width: 64, height: 64, color: '#b91c1c' }} />
|
||||||
if (state.overallUp === 0 && state.overallDown === 0) {
|
if (state.overallUp === 0 && state.overallDown === 0) {
|
||||||
|
|
@ -47,21 +48,28 @@ export default function OverallStatus({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (!isWindowVisible) {
|
if (!isWindowVisible) return
|
||||||
return
|
|
||||||
}
|
|
||||||
if (currentTime - state.lastUpdate > 300 && currentTime - openTime > 30) {
|
if (currentTime - state.lastUpdate > 300 && currentTime - openTime > 30) {
|
||||||
// trigger a re-fetch
|
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
setCurrentTime(Math.round(Date.now() / 1000))
|
setCurrentTime(Math.round(Date.now() / 1000))
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<Container size="md" mt="xl">
|
||||||
<Center>{icon}</Center>
|
<Center>{icon}</Center>
|
||||||
<Title mt="sm" style={{ textAlign: 'center' }} order={1}>
|
<Title mt="sm" style={{ textAlign: 'center' }} order={1}>
|
||||||
{statusString}
|
{statusString}
|
||||||
|
|
@ -72,6 +80,14 @@ export default function OverallStatus({
|
||||||
currentTime - state.lastUpdate
|
currentTime - state.lastUpdate
|
||||||
} sec ago)`}
|
} sec ago)`}
|
||||||
</Title>
|
</Title>
|
||||||
</>
|
|
||||||
|
{filteredMaintenances.map((maintenance, idx) => (
|
||||||
|
<MaintenanceAlert
|
||||||
|
key={idx}
|
||||||
|
maintenance={maintenance}
|
||||||
|
style={{ maxWidth: groupedMonitor ? '897px' : '865px' }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { workerConfig } from '@/uptime.config'
|
import { workerConfig } from '@/uptime.config'
|
||||||
import { MonitorState } from '@/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'
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
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 '@/uptime.types'
|
import { MonitorState, MonitorTarget } from '@/types/config'
|
||||||
import { KVNamespace } from '@cloudflare/workers-types'
|
import { KVNamespace } from '@cloudflare/workers-types'
|
||||||
import { pageConfig, workerConfig } from '@/uptime.config'
|
import { maintenances, pageConfig, workerConfig } from '@/uptime.config'
|
||||||
import OverallStatus from '@/components/OverallStatus'
|
import OverallStatus from '@/components/OverallStatus'
|
||||||
import Header from '@/components/Header'
|
import Header from '@/components/Header'
|
||||||
import MonitorList from '@/components/MonitorList'
|
import MonitorList from '@/components/MonitorList'
|
||||||
|
|
@ -51,7 +51,7 @@ export default function Home({
|
||||||
<main className={inter.className}>
|
<main className={inter.className}>
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
{state === undefined ? (
|
{state == undefined ? (
|
||||||
<Center>
|
<Center>
|
||||||
<Text fw={700}>
|
<Text fw={700}>
|
||||||
Monitor State is not defined now, please check your worker's status and KV
|
Monitor State is not defined now, please check your worker's status and KV
|
||||||
|
|
@ -60,7 +60,7 @@ export default function Home({
|
||||||
</Center>
|
</Center>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<OverallStatus state={state} />
|
<OverallStatus state={state} monitors={monitors} maintenances={maintenances} />
|
||||||
<MonitorList monitors={monitors} state={state} />
|
<MonitorList monitors={monitors} state={state} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
export type PageConfig = {
|
||||||
|
title?: string
|
||||||
|
links?: PageConfigLink[]
|
||||||
|
group?: PageConfigGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MaintenanceConfig = {
|
||||||
|
monitors?: string[]
|
||||||
|
title?: string
|
||||||
|
body: string
|
||||||
|
start: number | string
|
||||||
|
end?: number | string
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PageConfigGroup = { [key: string]: string[] }
|
||||||
|
|
||||||
|
export type PageConfigLink = {
|
||||||
|
link: string
|
||||||
|
label: string
|
||||||
|
highlight?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MonitorTarget = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
method: string
|
||||||
|
target: string
|
||||||
|
tooltip?: string
|
||||||
|
statusPageLink?: string
|
||||||
|
hideLatencyChart?: boolean
|
||||||
|
expectedCodes?: number[]
|
||||||
|
timeout?: number
|
||||||
|
headers?: { [key: string]: string | number }
|
||||||
|
body?: string
|
||||||
|
responseKeyword?: string
|
||||||
|
responseForbiddenKeyword?: string
|
||||||
|
checkProxy?: string
|
||||||
|
checkProxyFallback?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkerConfig = {
|
||||||
|
kvWriteCooldownMinutes: number
|
||||||
|
monitors: MonitorTarget[]
|
||||||
|
notification?: Notification
|
||||||
|
callbacks?: Callbacks
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Notification = {
|
||||||
|
appriseApiServer?: string
|
||||||
|
recipientUrl?: string
|
||||||
|
timeZone?: string
|
||||||
|
gracePeriod?: number
|
||||||
|
skipNotificationIds?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Callbacks = {
|
||||||
|
onStatusChange: (
|
||||||
|
env: any,
|
||||||
|
monitor: any,
|
||||||
|
isUp: boolean,
|
||||||
|
timeIncidentStart: number,
|
||||||
|
timeNow: number,
|
||||||
|
reason: string
|
||||||
|
) => Promise<any>
|
||||||
|
onIncident: (
|
||||||
|
env: any,
|
||||||
|
monitor: any,
|
||||||
|
timeIncidentStart: number,
|
||||||
|
timeNow: number,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
const pageConfig = {
|
import { MaintenanceConfig, PageConfig, WorkerConfig } from './types/config'
|
||||||
|
|
||||||
|
const pageConfig: PageConfig = {
|
||||||
// Title for your status page
|
// Title for your status page
|
||||||
title: "lyc8503's Status Page",
|
title: "lyc8503's Status Page",
|
||||||
// Links shown at the header of your status page, could set `highlight` to `true`
|
// Links shown at the header of your status page, could set `highlight` to `true`
|
||||||
|
|
@ -16,7 +18,7 @@ const pageConfig = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const workerConfig = {
|
const workerConfig: WorkerConfig = {
|
||||||
// Write KV at most every 3 minutes unless the status changed
|
// Write KV at most every 3 minutes unless the status changed
|
||||||
kvWriteCooldownMinutes: 3,
|
kvWriteCooldownMinutes: 3,
|
||||||
// Enable HTTP Basic auth for status page & API by uncommenting the line below, format `<USERNAME>:<PASSWORD>`
|
// Enable HTTP Basic auth for status page & API by uncommenting the line below, format `<USERNAME>:<PASSWORD>`
|
||||||
|
|
@ -117,5 +119,28 @@ const workerConfig = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// You can define multiple maintenances here
|
||||||
|
// During maintenance, an alert will be shown at status page
|
||||||
|
// Also, related downtime notifications will be skipped (if any)
|
||||||
|
// Of course, you can leave it empty if you don't need this feature
|
||||||
|
// const maintenances: MaintenanceConfig[] = []
|
||||||
|
const maintenances: MaintenanceConfig[] = [
|
||||||
|
{
|
||||||
|
// [Optional] Monitor IDs to be affected by this maintenance
|
||||||
|
monitors: ['foo_monitor', 'bar_monitor'],
|
||||||
|
// [Optional] default to "Scheduled Maintenance" if not specified
|
||||||
|
title: 'Test Maintenance',
|
||||||
|
// Description of the maintenance, will be shown at status page
|
||||||
|
body: 'This is a test maintenance, server software upgrade',
|
||||||
|
// Start time of the maintenance, in UNIX timestamp or ISO 8601 format
|
||||||
|
start: '2025-04-27T00:00:00+08:00',
|
||||||
|
// [Optional] end time of the maintenance, in UNIX timestamp or ISO 8601 format
|
||||||
|
// if not specified, the maintenance will be considered as on-going
|
||||||
|
end: '2025-04-30T00:00:00+08:00',
|
||||||
|
// [Optional] color of the maintenance alert at status page, default to "yellow"
|
||||||
|
color: 'blue',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
// Don't forget this, otherwise compilation fails.
|
// Don't forget this, otherwise compilation fails.
|
||||||
export { pageConfig, workerConfig }
|
export { pageConfig, workerConfig, maintenances }
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
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 = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
method: string // "TCP_PING" or Http Method (e.g. GET, POST, OPTIONS, etc.)
|
|
||||||
target: string // url for http, hostname:port for tcp
|
|
||||||
tooltip?: string
|
|
||||||
statusPageLink?: string
|
|
||||||
hideLatencyChart?: boolean
|
|
||||||
checkProxy?: string
|
|
||||||
checkProxyFallback?: boolean
|
|
||||||
|
|
||||||
// HTTP Code
|
|
||||||
expectedCodes?: number[]
|
|
||||||
timeout?: number
|
|
||||||
headers?: Record<string, string | undefined>
|
|
||||||
body?: BodyInit
|
|
||||||
responseKeyword?: string
|
|
||||||
responseForbiddenKeyword?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { MonitorState, MonitorTarget }
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { workerConfig } 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 '../../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'
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ export interface Env {
|
||||||
REMOTE_CHECKER_DO: DurableObjectNamespace<RemoteChecker>
|
REMOTE_CHECKER_DO: DurableObjectNamespace<RemoteChecker>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
const Worker = {
|
||||||
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
|
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
|
||||||
const workerLocation = (await getWorkerLocation()) || 'ERROR'
|
const workerLocation = (await getWorkerLocation()) || 'ERROR'
|
||||||
console.log(`Running scheduled event on ${workerLocation}...`)
|
console.log(`Running scheduled event on ${workerLocation}...`)
|
||||||
|
|
@ -23,8 +23,7 @@ export default {
|
||||||
reason: string
|
reason: string
|
||||||
) => {
|
) => {
|
||||||
// Skip notification if monitor is in the skip list
|
// Skip notification if monitor is in the skip list
|
||||||
// @ts-ignore
|
const skipList = workerConfig.notification?.skipNotificationIds
|
||||||
const skipList: string[] = workerConfig.notification?.skipNotificationIds
|
|
||||||
if (skipList && skipList.includes(monitor.id)) {
|
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)`
|
||||||
|
|
@ -32,6 +31,21 @@ export default {
|
||||||
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,
|
||||||
|
|
@ -165,7 +179,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Calling config onStatusChange callback...')
|
console.log('Calling config onStatusChange callback...')
|
||||||
await workerConfig.callbacks.onStatusChange(
|
await workerConfig.callbacks?.onStatusChange(
|
||||||
env,
|
env,
|
||||||
monitor,
|
monitor,
|
||||||
true,
|
true,
|
||||||
|
|
@ -236,7 +250,7 @@ export default {
|
||||||
|
|
||||||
if (monitorStatusChanged) {
|
if (monitorStatusChanged) {
|
||||||
console.log('Calling config onStatusChange callback...')
|
console.log('Calling config onStatusChange callback...')
|
||||||
await workerConfig.callbacks.onStatusChange(
|
await workerConfig.callbacks?.onStatusChange(
|
||||||
env,
|
env,
|
||||||
monitor,
|
monitor,
|
||||||
false,
|
false,
|
||||||
|
|
@ -252,7 +266,7 @@ export default {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Calling config onIncident callback...')
|
console.log('Calling config onIncident callback...')
|
||||||
await workerConfig.callbacks.onIncident(
|
await workerConfig.callbacks?.onIncident(
|
||||||
env,
|
env,
|
||||||
monitor,
|
monitor,
|
||||||
currentIncident.start[0],
|
currentIncident.start[0],
|
||||||
|
|
@ -329,6 +343,8 @@ export default {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Worker
|
||||||
|
|
||||||
export class RemoteChecker extends DurableObject {
|
export class RemoteChecker extends DurableObject {
|
||||||
constructor(ctx: DurableObjectState, env: Env) {
|
constructor(ctx: DurableObjectState, env: Env) {
|
||||||
super(ctx, env)
|
super(ctx, env)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue