diff --git a/components/Header.tsx b/components/Header.tsx index 2e6a149..4504e38 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,6 +1,6 @@ import { Container, Group, Text } from '@mantine/core' import classes from '@/styles/Header.module.css' -import { pageConfig } from '@/uptime.config' +import { pageConfig } from '@/config' import { PageConfigLink } from '@/types/config' export default function Header() { diff --git a/components/MonitorList.tsx b/components/MonitorList.tsx index ac33ea5..71af365 100644 --- a/components/MonitorList.tsx +++ b/components/MonitorList.tsx @@ -1,7 +1,7 @@ -import { MonitorState, MonitorTarget } from '@/uptime.types' +import { MonitorState, MonitorTarget } from '@/types/uptime.types' import { Accordion, Card, Center, Text } from '@mantine/core' import MonitorDetail from './MonitorDetail' -import { pageConfig } from '@/uptime.config'; +import { pageConfig } from '@/config' function countDownCount(state: MonitorState, ids: string[]) { let downCount = 0 @@ -28,45 +28,61 @@ function getStatusTextColor(state: MonitorState, ids: string[]) { } } -export default function MonitorList({ monitors, state }: { monitors: MonitorTarget[]; state: MonitorState }) { +export default function MonitorList({ + monitors, + state, +}: { + monitors: MonitorTarget[] + state: MonitorState +}) { // @ts-ignore let group: any = pageConfig.group let groupedMonitor = group && Object.keys(group).length > 0 let content - + if (groupedMonitor) { // Grouped monitors content = ( - - { - Object.keys(group).map(groupName => ( - - -
-
{groupName}
- - {group[groupName].length - countDownCount(state, group[groupName])} - /{group[groupName].length} Operational - -
-
- - { - monitors - .filter(monitor => group[groupName].includes(monitor.id)) - .sort((a, b) => group[groupName].indexOf(a.id) - group[groupName].indexOf(b.id)) - .map(monitor => ( -
- - - -
- )) - } -
-
- )) - } + + {Object.keys(group).map((groupName) => ( + + +
+
{groupName}
+ + {group[groupName].length - countDownCount(state, group[groupName])}/ + {group[groupName].length} Operational + +
+
+ + {monitors + .filter((monitor) => group[groupName].includes(monitor.id)) + .sort((a, b) => group[groupName].indexOf(a.id) - group[groupName].indexOf(b.id)) + .map((monitor) => ( +
+ + + +
+ ))} +
+
+ ))}
) } else { diff --git a/config/index.ts b/config/index.ts new file mode 100644 index 0000000..e108389 --- /dev/null +++ b/config/index.ts @@ -0,0 +1,13 @@ +import { Maintenances, PageConfig, WorkerConfig } from '@/types/config' +import * as config from '@/uptime.config' + +const pageConfig = config.pageConfig +const workerConfig = config.workerConfig + +let maintenances = [] +// need any because we cannot guarantee that maintenances exists +if ((config as any).maintenances) { + maintenances = (config as any).maintenances +} + +export { pageConfig, workerConfig, maintenances } diff --git a/middleware.ts b/middleware.ts index 78ec4d8..a38e1cb 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' -import { workerConfig } from './uptime.config' - +import { workerConfig } from '@/config' + export async function middleware(request: NextRequest) { // @ts-ignore const passwordProtection = workerConfig.passwordProtection @@ -12,15 +12,16 @@ export async function middleware(request: NextRequest) { if (authHeader && authHeader.length === expected.length) { // a simple timing-safe compare - authenticated = true; + authenticated = true for (let i = 0; i < authHeader.length; i++) { - if (authHeader[i] !== expected[i]) authenticated = false; + if (authHeader[i] !== expected[i]) authenticated = false } } if (!authenticated) { return NextResponse.json( - { code: 401, message: "Not authenticated" }, { status: 401, headers: { 'WWW-Authenticate': 'Basic' } } + { code: 401, message: 'Not authenticated' }, + { status: 401, headers: { 'WWW-Authenticate': 'Basic' } } ) } } diff --git a/pages/api/data.ts b/pages/api/data.ts index b2d29b5..9415cf8 100644 --- a/pages/api/data.ts +++ b/pages/api/data.ts @@ -1,4 +1,4 @@ -import { workerConfig } from '@/uptime.config' +import { workerConfig } from '@/config' import { MonitorState } from '@/types/uptime.types' import { NextRequest } from 'next/server' diff --git a/pages/index.tsx b/pages/index.tsx index afc9ec6..06e4ef7 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,9 +1,9 @@ import Head from 'next/head' import { Inter } from 'next/font/google' -import { MonitorState, MonitorTarget } from '@/uptime.types' +import { MonitorState, MonitorTarget } from '@/types/uptime.types' import { KVNamespace } from '@cloudflare/workers-types' -import { pageConfig, workerConfig } from '@/uptime.config' +import { pageConfig, workerConfig } from '@/config' import OverallStatus from '@/components/OverallStatus' import Header from '@/components/Header' import MonitorList from '@/components/MonitorList' @@ -22,21 +22,17 @@ export default function Home({ tooltip?: string statusPageLink?: string }) { - let state; + let state if (stateStr !== undefined) { state = JSON.parse(stateStr) as MonitorState } // Specify monitorId in URL hash to view a specific monitor (can be used in iframe) - const monitorId = window.location.hash.substring(1); + const monitorId = window.location.hash.substring(1) if (monitorId) { - const monitor = monitors.find((monitor) => monitor.id === monitorId); + const monitor = monitors.find((monitor) => monitor.id === monitorId) if (!monitor || !state) { - return ( - - Monitor with id {monitorId} not found! - - ) + return Monitor with id {monitorId} not found! } return (
@@ -70,9 +66,14 @@ export default function Home({ )} - + Open-source monitoring and status page powered by{' '} Uptimeflare diff --git a/types/config.ts b/types/config.ts index 01644ea..9893628 100644 --- a/types/config.ts +++ b/types/config.ts @@ -2,7 +2,6 @@ export type PageConfig = { title?: string links?: PageConfigLink[] group?: PageConfigGroup - maintenances?: Maintenances[] } export type Maintenances = { diff --git a/uptime.config.ts b/uptime.config.ts index 2ccaeb7..e07a2b5 100644 --- a/uptime.config.ts +++ b/uptime.config.ts @@ -1,4 +1,4 @@ -import { PageConfig, WorkerConfig } from './types/config' +import { Maintenances, PageConfig, WorkerConfig } from '@/types/config' const pageConfig: PageConfig = { // Title for your status page @@ -119,5 +119,16 @@ const workerConfig: WorkerConfig = { }, } +const maintenances: Maintenances[] = [ + // { + // title: 'Planned downtime', + // body: 'Please be patient', + // monitors: ['monitor-id-1', 'monitor-id-2'], + // start: new Date('2025-04-19 00:00:00Z'), + // end: new Date('2025-04-19 01:00:00Z'), + // color: 'red', + // }, +] + // Don't forget this, otherwise compilation fails. -export { pageConfig, workerConfig } +export { pageConfig, workerConfig, maintenances } diff --git a/worker/src/index.ts b/worker/src/index.ts index 74fc868..86fb07e 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -1,6 +1,6 @@ -import { workerConfig } from '../../uptime.config' +import { workerConfig } from '../../config' import { formatStatusChangeNotification, getWorkerLocation, notifyWithApprise } from './util' -import { MonitorState, MonitorTarget } from '../../uptime.types' +import { MonitorState, MonitorTarget } from '../../types/uptime.types' import { getStatus } from './monitor' import { DurableObject } from 'cloudflare:workers' @@ -26,7 +26,9 @@ export default { // @ts-ignore const skipList: string[] = workerConfig.notification?.skipNotificationIds if (skipList && skipList.includes(monitor.id)) { - console.log(`Skipping notification for ${monitor.name} (${monitor.id} in skipNotificationIds)`) + console.log( + `Skipping notification for ${monitor.name} (${monitor.id} in skipNotificationIds)` + ) return } @@ -46,7 +48,9 @@ export default { notification.body ) } else { - console.log(`Apprise API server or recipient URL not set, skipping apprise notification for ${monitor.name}`) + console.log( + `Apprise API server or recipient URL not set, skipping apprise notification for ${monitor.name}` + ) } } @@ -83,11 +87,11 @@ export default { try { console.log('Calling check proxy: ' + monitor.checkProxy) let resp - if (monitor.checkProxy.startsWith("worker://")) { - const doLoc = monitor.checkProxy.replace("worker://", "") + if (monitor.checkProxy.startsWith('worker://')) { + const doLoc = monitor.checkProxy.replace('worker://', '') const doId = env.REMOTE_CHECKER_DO.idFromName(doLoc) const doStub = env.REMOTE_CHECKER_DO.get(doId, { - locationHint: doLoc as DurableObjectLocationHint + locationHint: doLoc as DurableObjectLocationHint, }) resp = await doStub.getLocationAndStatus(monitor) } else { @@ -97,7 +101,7 @@ export default { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(monitor), }) - ).json<{location: string; status: {ping: number; up: boolean; err: string}}>() + ).json<{ location: string; status: { ping: number; up: boolean; err: string } }>() } checkLocation = resp.location status = resp.status @@ -144,17 +148,14 @@ export default { // grace period not set OR ... workerConfig.notification?.gracePeriod === undefined || // only when we have sent a notification for DOWN status, we will send a notification for UP status (within 30 seconds of possible drift) - currentTimeSecond - lastIncident.start[0] >= (workerConfig.notification.gracePeriod + 1) * 60 - 30 + currentTimeSecond - lastIncident.start[0] >= + (workerConfig.notification.gracePeriod + 1) * 60 - 30 ) { - await formatAndNotify( - monitor, - true, - lastIncident.start[0], - currentTimeSecond, - 'OK' - ) + await formatAndNotify(monitor, true, lastIncident.start[0], currentTimeSecond, 'OK') } else { - console.log(`grace period (${workerConfig.notification?.gracePeriod}m) not met, skipping apprise UP notification for ${monitor.name}`) + console.log( + `grace period (${workerConfig.notification?.gracePeriod}m) not met, skipping apprise UP notification for ${monitor.name}` + ) } console.log('Calling config onStatusChange callback...') @@ -195,22 +196,20 @@ export default { try { if ( // monitor status changed AND... - (monitorStatusChanged && ( + (monitorStatusChanged && // grace period not set OR ... - workerConfig.notification?.gracePeriod === undefined || - // have sent a notification for DOWN status - currentTimeSecond - currentIncident.start[0] >= (workerConfig.notification.gracePeriod + 1) * 60 - 30 - )) - || - ( - // grace period is set AND... - workerConfig.notification?.gracePeriod !== undefined && - ( - // grace period is met - currentTimeSecond - currentIncident.start[0] >= workerConfig.notification.gracePeriod * 60 - 30 && - currentTimeSecond - currentIncident.start[0] < workerConfig.notification.gracePeriod * 60 + 30 - ) - )) { + (workerConfig.notification?.gracePeriod === undefined || + // have sent a notification for DOWN status + currentTimeSecond - currentIncident.start[0] >= + (workerConfig.notification.gracePeriod + 1) * 60 - 30)) || + // grace period is set AND... + (workerConfig.notification?.gracePeriod !== undefined && + // grace period is met + currentTimeSecond - currentIncident.start[0] >= + workerConfig.notification.gracePeriod * 60 - 30 && + currentTimeSecond - currentIncident.start[0] < + workerConfig.notification.gracePeriod * 60 + 30) + ) { await formatAndNotify( monitor, false, @@ -219,7 +218,14 @@ export default { status.err ) } else { - console.log(`Grace period (${workerConfig.notification?.gracePeriod}m) not met (currently down for ${currentTimeSecond - currentIncident.start[0]}s, changed ${monitorStatusChanged}), skipping apprise DOWN notification for ${monitor.name}`) + console.log( + `Grace period (${workerConfig.notification + ?.gracePeriod}m) not met (currently down for ${ + currentTimeSecond - currentIncident.start[0] + }s, changed ${monitorStatusChanged}), skipping apprise DOWN notification for ${ + monitor.name + }` + ) } if (monitorStatusChanged) { @@ -274,40 +280,45 @@ export default { // discard old incidents let incidentList = state.incident[monitor.id] - while (incidentList.length > 0 && incidentList[0].end && incidentList[0].end < currentTimeSecond - 90 * 24 * 60 * 60) { + while ( + incidentList.length > 0 && + incidentList[0].end && + incidentList[0].end < currentTimeSecond - 90 * 24 * 60 * 60 + ) { incidentList.shift() } - if (incidentList.length == 0 || ( - incidentList[0].start[0] > currentTimeSecond - 90 * 24 * 60 * 60 && - incidentList[0].error[0] != 'dummy' - )) { + if ( + incidentList.length == 0 || + (incidentList[0].start[0] > currentTimeSecond - 90 * 24 * 60 * 60 && + incidentList[0].error[0] != 'dummy') + ) { // put the dummy incident back - incidentList.unshift( - { - start: [currentTimeSecond - 90 * 24 * 60 * 60], - end: currentTimeSecond - 90 * 24 * 60 * 60, - error: ['dummy'], - } - ) + incidentList.unshift({ + start: [currentTimeSecond - 90 * 24 * 60 * 60], + end: currentTimeSecond - 90 * 24 * 60 * 60, + error: ['dummy'], + }) } state.incident[monitor.id] = incidentList statusChanged ||= monitorStatusChanged } - console.log(`statusChanged: ${statusChanged}, lastUpdate: ${state.lastUpdate}, currentTime: ${currentTimeSecond}`) + console.log( + `statusChanged: ${statusChanged}, lastUpdate: ${state.lastUpdate}, currentTime: ${currentTimeSecond}` + ) // Update state // Allow for a cooldown period before writing to KV if ( statusChanged || - currentTimeSecond - state.lastUpdate >= workerConfig.kvWriteCooldownMinutes * 60 - 10 // Allow for 10 seconds of clock drift + currentTimeSecond - state.lastUpdate >= workerConfig.kvWriteCooldownMinutes * 60 - 10 // Allow for 10 seconds of clock drift ) { - console.log("Updating state...") + console.log('Updating state...') state.lastUpdate = currentTimeSecond await env.UPTIMEFLARE_STATE.put('state', JSON.stringify(state)) } else { - console.log("Skipping state update due to cooldown period.") + console.log('Skipping state update due to cooldown period.') } }, } @@ -317,8 +328,10 @@ export class RemoteChecker extends DurableObject { super(ctx, env) } - async getLocationAndStatus(monitor: MonitorTarget): Promise<{location: string; status: {ping: number; up: boolean; err: string}}> { - const colo = await getWorkerLocation() as string + async getLocationAndStatus( + monitor: MonitorTarget + ): Promise<{ location: string; status: { ping: number; up: boolean; err: string } }> { + const colo = (await getWorkerLocation()) as string console.log(`Running remote checker (DurableObject) at ${colo}...`) const status = await getStatus(monitor) return {