feat: move types

pull/101/head
Bennet Gallein 2025-04-18 18:13:04 +02:00
parent 751373d5dd
commit 5551bb554a
No known key found for this signature in database
GPG Key ID: 54F2DF6954E8C635
7 changed files with 135 additions and 49 deletions

View File

@ -1,4 +1,4 @@
import { MonitorState, MonitorTarget } from '@/uptime.types' import { MonitorState, MonitorTarget } from '@/types/uptime.types'
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'
@ -50,13 +50,20 @@ export default function DetailBar({
if (overlap > 0) { if (overlap > 0) {
for (let i = 0; i < incident.error.length; i++) { for (let i = 0; i < incident.error.length; i++) {
let partStart = incident.start[i] let partStart = incident.start[i]
let partEnd = i === incident.error.length - 1 ? (incident.end ?? currentTime) : incident.start[i + 1] let partEnd =
i === incident.error.length - 1 ? incident.end ?? currentTime : incident.start[i + 1]
partStart = Math.max(partStart, dayStart) partStart = Math.max(partStart, dayStart)
partEnd = Math.min(partEnd, dayEnd) partEnd = Math.min(partEnd, dayEnd)
if (overlapLen(dayStart, dayEnd, partStart, partEnd) > 0) { if (overlapLen(dayStart, dayEnd, partStart, partEnd) > 0) {
const startStr = new Date(partStart * 1000).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) const startStr = new Date(partStart * 1000).toLocaleTimeString([], {
const endStr = new Date(partEnd * 1000).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) hour: '2-digit',
minute: '2-digit',
})
const endStr = new Date(partEnd * 1000).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})
incidentReasons.push(`[${startStr}-${endStr}] ${incident.error[i]}`) incidentReasons.push(`[${startStr}-${endStr}] ${incident.error[i]}`)
} }
} }
@ -77,7 +84,10 @@ export default function DetailBar({
<> <>
<div>{dayPercent + '% at ' + new Date(dayStart * 1000).toLocaleDateString()}</div> <div>{dayPercent + '% at ' + new Date(dayStart * 1000).toLocaleDateString()}</div>
{dayDownTime > 0 && ( {dayDownTime > 0 && (
<div>{`Down for ${moment.preciseDiff(moment(0), moment(dayDownTime * 1000))} (click for detail)`}</div> <div>{`Down for ${moment.preciseDiff(
moment(0),
moment(dayDownTime * 1000)
)} (click for detail)`}</div>
)} )}
</> </>
) )
@ -94,10 +104,14 @@ export default function DetailBar({
}} }}
onClick={() => { onClick={() => {
if (dayDownTime > 0) { if (dayDownTime > 0) {
setModalTitle(`🚨 ${monitor.name} incidents at ${new Date(dayStart * 1000).toLocaleDateString()}`) setModalTitle(
`🚨 ${monitor.name} incidents at ${new Date(dayStart * 1000).toLocaleDateString()}`
)
setModelContent( setModelContent(
<> <>
{incidentReasons.map((reason, index) => (<div key={index}>{reason}</div>))} {incidentReasons.map((reason, index) => (
<div key={index}>{reason}</div>
))}
</> </>
) )
setModalOpened(true) setModalOpened(true)
@ -110,7 +124,12 @@ export default function DetailBar({
return ( return (
<> <>
<Modal opened={modalOpened} onClose={() => setModalOpened(false)} title={modalTitle} size={'40em'}> <Modal
opened={modalOpened}
onClose={() => setModalOpened(false)}
title={modalTitle}
size={'40em'}
>
{modelContent} {modelContent}
</Modal> </Modal>
<Box <Box

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 '@/uptime.types' import { MonitorState, MonitorTarget } from '@/types/uptime.types'
import { iataToCountry } from '@/util/iata' import { iataToCountry } from '@/util/iata'
ChartJS.register( ChartJS.register(

View File

@ -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>

View File

@ -1,11 +1,10 @@
import { workerConfig } from "@/uptime.config" import { workerConfig } from '@/uptime.config'
import { MonitorState } from "@/uptime.types" import { MonitorState } from '@/types/uptime.types'
import { NextRequest } from "next/server" import { NextRequest } from 'next/server'
export const runtime = 'edge' export const runtime = 'edge'
export default async function handler(req: NextRequest): Promise<Response> { export default async function handler(req: NextRequest): Promise<Response> {
const { UPTIMEFLARE_STATE } = process.env as unknown as { const { UPTIMEFLARE_STATE } = process.env as unknown as {
UPTIMEFLARE_STATE: KVNamespace UPTIMEFLARE_STATE: KVNamespace
} }
@ -15,11 +14,11 @@ export default async function handler(req: NextRequest): Promise<Response> {
return new Response(JSON.stringify({ error: 'No data available' }), { return new Response(JSON.stringify({ error: 'No data available' }), {
status: 500, status: 500,
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
} },
}) })
} }
const state = (JSON.parse(stateStr)) as unknown as MonitorState const state = JSON.parse(stateStr) as unknown as MonitorState
let monitors: any = {} let monitors: any = {}
@ -29,7 +28,7 @@ export default async function handler(req: NextRequest): Promise<Response> {
up: isUp, up: isUp,
latency: state.latency[monitor.id].recent.slice(-1)[0].ping, latency: state.latency[monitor.id].recent.slice(-1)[0].ping,
location: state.latency[monitor.id].recent.slice(-1)[0].loc, location: state.latency[monitor.id].recent.slice(-1)[0].loc,
message: isUp ? "OK" : state.incident[monitor.id].slice(-1)[0].error.slice(-1)[0] message: isUp ? 'OK' : state.incident[monitor.id].slice(-1)[0].error.slice(-1)[0],
} }
} }
@ -37,12 +36,12 @@ export default async function handler(req: NextRequest): Promise<Response> {
up: state.overallUp, up: state.overallUp,
down: state.overallDown, down: state.overallDown,
updatedAt: state.lastUpdate, updatedAt: state.lastUpdate,
monitors: monitors monitors: monitors,
} }
return new Response(JSON.stringify(ret), { return new Response(JSON.stringify(ret), {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
} },
}) })
} }

80
types/config.ts Normal file
View File

@ -0,0 +1,80 @@
export type PageConfig = {
title?: string
links?: PageConfigLink[]
group?: PageConfigGroup
maintenances?: Maintenances[]
}
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
color?: string
}
export type PageConfigGroup = { [key: string]: string[] }
export type PageConfigLink = {
link: string
label: string
highlight?: boolean
}
export type WorkerConfig = {
kvWriteCooldownMinutes: number
monitors: Monitor[]
notification?: Notification
callbacks?: Callbacks
}
export type Monitor = {
id: string
name: string
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'TCP_PING'
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 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>
}

View File

@ -1,3 +1,5 @@
import { Monitor } from './config'
type MonitorState = { type MonitorState = {
lastUpdate: number lastUpdate: number
overallUp: number overallUp: number
@ -28,24 +30,8 @@ type MonitorState = {
> >
} }
type MonitorTarget = { type MonitorTarget = Monitor & {
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 body?: BodyInit
responseKeyword?: string
responseForbiddenKeyword?: string
} }
export type { MonitorState, MonitorTarget } export type { MonitorState, MonitorTarget }

View File

@ -1,4 +1,6 @@
const pageConfig = { import { 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`
@ -11,12 +13,12 @@ const pageConfig = {
// If not specified, all monitors will be shown in a single list // If not specified, all monitors will be shown in a single list
// If specified, monitors will be grouped and ordered, not-listed monitors will be invisble (but still monitored) // If specified, monitors will be grouped and ordered, not-listed monitors will be invisble (but still monitored)
group: { group: {
"🌐 Public (example group name)": ['foo_monitor', 'bar_monitor', 'more monitor ids...'], '🌐 Public (example group name)': ['foo_monitor', 'bar_monitor', 'more monitor ids...'],
"🔐 Private": ['test_tcp_monitor'], '🔐 Private': ['test_tcp_monitor'],
}, },
} }
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>`
@ -77,12 +79,12 @@ const workerConfig = {
notification: { notification: {
// [Optional] apprise API server URL // [Optional] apprise API server URL
// if not specified, no notification will be sent // if not specified, no notification will be sent
appriseApiServer: "https://apprise.example.com/notify", appriseApiServer: 'https://apprise.example.com/notify',
// [Optional] recipient URL for apprise, refer to https://github.com/caronc/apprise // [Optional] recipient URL for apprise, refer to https://github.com/caronc/apprise
// if not specified, no notification will be sent // if not specified, no notification will be sent
recipientUrl: "tgram://bottoken/ChatID", recipientUrl: 'tgram://bottoken/ChatID',
// [Optional] timezone used in notification messages, default to "Etc/GMT" // [Optional] timezone used in notification messages, default to "Etc/GMT"
timeZone: "Asia/Shanghai", timeZone: 'Asia/Shanghai',
// [Optional] grace period in minutes before sending a notification // [Optional] grace period in minutes before sending a notification
// notification will be sent only if the monitor is down for N continuous checks after the initial failure // notification will be sent only if the monitor is down for N continuous checks after the initial failure
// if not specified, notification will be sent immediately // if not specified, notification will be sent immediately
@ -101,7 +103,6 @@ const workerConfig = {
) => { ) => {
// This callback will be called when there's a status change for any monitor // This callback will be called when there's a status change for any monitor
// Write any Typescript code here // Write any Typescript code here
// This will not follow the grace period settings and will be called immediately when the status changes // This will not follow the grace period settings and will be called immediately when the status changes
// You need to handle the grace period manually if you want to implement it // You need to handle the grace period manually if you want to implement it
}, },