Initial commit

This commit is contained in:
2026-07-29 11:35:11 +08:00
committed by GitHub
commit a158df3216
72 changed files with 24626 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
import '@mantine/core/styles.css'
import type { AppProps } from 'next/app'
import { MantineProvider } from '@mantine/core'
import NoSsr from '@/components/NoSsr'
import '@/util/i18n'
export default function App({ Component, pageProps }: AppProps) {
return (
<NoSsr>
<MantineProvider defaultColorScheme="auto">
<Component {...pageProps} />
</MantineProvider>
</NoSsr>
)
}
+16
View File
@@ -0,0 +1,16 @@
import { Html, Head, Main, NextScript } from 'next/document'
import { ColorSchemeScript } from '@mantine/core'
export default function Document() {
return (
<Html lang="en">
<Head>
<ColorSchemeScript defaultColorScheme="auto" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
+75
View File
@@ -0,0 +1,75 @@
import { NextRequest } from 'next/server'
import { CompactedMonitorStateWrapper, getFromStore } from '@/worker/src/store'
export const runtime = 'edge'
type BadgePayload = {
schemaVersion: 1
label: string
message: string
color: string
isError?: boolean
}
const jsonHeaders = {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, max-age=0, must-revalidate',
}
function errorBadge(label: string, message: string): BadgePayload {
return {
schemaVersion: 1,
label,
message,
color: 'lightgrey',
isError: true,
}
}
export default async function handler(req: NextRequest): Promise<Response> {
try {
const url = new URL(req.url)
const monitorId = url.searchParams.get('id')
const label = url.searchParams.get('label') ?? monitorId ?? 'UptimeFlare'
const upMsg = url.searchParams.get('up') ?? 'UP'
const downMsg = url.searchParams.get('down') ?? 'DOWN'
const colorUp = url.searchParams.get('colorUp') ?? 'brightgreen'
const colorDown = url.searchParams.get('colorDown') ?? 'red'
if (!monitorId) {
return new Response(JSON.stringify(errorBadge(label, 'no-monitor')), {
headers: jsonHeaders,
status: 400,
})
}
const compactedState = new CompactedMonitorStateWrapper(
await getFromStore(process.env as any, 'state')
)
const lastIncident = compactedState.getIncident(
monitorId,
compactedState.incidentLen(monitorId) - 1
)
const isUp = lastIncident?.end !== null
const badge: BadgePayload = {
schemaVersion: 1,
label,
message: isUp ? upMsg : downMsg,
color: isUp ? colorUp : colorDown,
}
return new Response(JSON.stringify(badge), {
headers: jsonHeaders,
})
} catch (err) {
console.error('Error rendering badge API:', err)
return new Response(JSON.stringify(errorBadge('status', 'error')), {
headers: jsonHeaders,
status: 500,
})
}
}
+55
View File
@@ -0,0 +1,55 @@
import { maintenances, workerConfig } from '@/uptime.config'
import { NextRequest } from 'next/server'
import { CompactedMonitorStateWrapper, getFromStore } from '@/worker/src/store'
export const runtime = 'edge'
const headers = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
}
export default async function handler(req: NextRequest): Promise<Response> {
const compactedState = new CompactedMonitorStateWrapper(
await getFromStore(process.env as any, 'state')
)
if (compactedState.data.lastUpdate === 0) {
return new Response(JSON.stringify({ error: 'No data available' }), {
status: 500,
headers,
})
}
let monitors: any = {}
for (let monitor of workerConfig.monitors) {
const lastIncident = compactedState.getIncident(
monitor.id,
compactedState.incidentLen(monitor.id) - 1
)
const isUp = lastIncident?.end !== null
const latency = compactedState.getLastLatency(monitor.id)
monitors[monitor.id] = {
up: isUp,
latency: latency.ping,
location: latency.loc,
message: isUp ? 'OK' : lastIncident?.error[lastIncident.error.length - 1],
}
}
let ret = {
up: compactedState.data.overallUp,
down: compactedState.data.overallDown,
updatedAt: compactedState.data.lastUpdate,
monitors,
maintenances,
}
return new Response(JSON.stringify(ret), {
headers,
})
}
+144
View File
@@ -0,0 +1,144 @@
import Head from 'next/head'
import { Inter } from 'next/font/google'
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
import { maintenances, pageConfig } from '@/uptime.config'
import Header from '@/components/Header'
import { Box, Button, Center, Container, Group, Select } from '@mantine/core'
import Footer from '@/components/Footer'
import { useEffect, useState } from 'react'
import MaintenanceAlert from '@/components/MaintenanceAlert'
import NoIncidentsAlert from '@/components/NoIncidents'
import { useTranslation } from 'react-i18next'
export const runtime = 'experimental-edge'
const inter = Inter({ subsets: ['latin'] })
function getSelectedMonth() {
const hash = window.location.hash.replace('#', '')
if (!hash) {
const now = new Date()
return now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0')
}
return hash.split('-').splice(0, 2).join('-')
}
function filterIncidentsByMonth(
incidents: MaintenanceConfig[],
monthStr: string,
monitors: MonitorTarget[]
): (Omit<MaintenanceConfig, 'monitors'> & { monitors: MonitorTarget[] })[] {
return incidents
.filter((incident) => {
const d = new Date(incident.start)
const incidentMonth = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0')
return incidentMonth === monthStr
})
.map((e) => ({
...e,
monitors: (e.monitors || []).map((e) => monitors.find((mon) => mon.id === e)!),
}))
.sort((a, b) => (new Date(a.start) > new Date(b.start) ? -1 : 1))
}
function getPrevNextMonth(monthStr: string) {
const [year, month] = monthStr.split('-').map(Number)
const date = new Date(year, month - 1)
const prev = new Date(date)
prev.setMonth(prev.getMonth() - 1)
const next = new Date(date)
next.setMonth(next.getMonth() + 1)
return {
prev: prev.getFullYear() + '-' + String(prev.getMonth() + 1).padStart(2, '0'),
next: next.getFullYear() + '-' + String(next.getMonth() + 1).padStart(2, '0'),
}
}
export default function IncidentsPage({ monitors }: { monitors: MonitorTarget[] }) {
const { t } = useTranslation('common')
const [selectedMonitor, setSelectedMonitor] = useState<string | null>('')
const [selectedMonth, setSelectedMonth] = useState(getSelectedMonth())
useEffect(() => {
const onHashChange = () => setSelectedMonth(getSelectedMonth())
window.addEventListener('hashchange', onHashChange)
return () => window.removeEventListener('hashchange', onHashChange)
}, [])
const filteredIncidents = filterIncidentsByMonth(maintenances, selectedMonth, monitors)
const monitorFilteredIncidents = selectedMonitor
? filteredIncidents.filter((i) => i.monitors.find((e) => e.id === selectedMonitor))
: filteredIncidents
const { prev, next } = getPrevNextMonth(selectedMonth)
const monitorOptions = [
{ value: '', label: t('All') },
...monitors.map((monitor) => ({
value: monitor.id,
label: monitor.name,
})),
]
return (
<>
<Head>
<title>{pageConfig.title}</title>
<link rel="icon" href={pageConfig.favicon ?? '/favicon.png'} />
</Head>
<main className={inter.className}>
<Header
style={{
marginBottom: '40px',
}}
/>
<Center>
<Container size="md" style={{ width: '100%' }}>
<Group justify="end" mb="md">
<Select
placeholder={t('Select monitor')}
data={monitorOptions}
value={selectedMonitor}
onChange={setSelectedMonitor}
clearable
style={{ maxWidth: 300, float: 'right' }}
/>
</Group>
<Box>
{monitorFilteredIncidents.length === 0 ? (
<NoIncidentsAlert />
) : (
monitorFilteredIncidents.map((incident, i) => (
<MaintenanceAlert key={i} maintenance={incident} />
))
)}
</Box>
<Group justify="space-between" mt="md">
<Button variant="default" onClick={() => (window.location.hash = prev)}>
{t('Backwards')}
</Button>
<Box style={{ alignSelf: 'center', fontWeight: 500, fontSize: 18 }}>
{selectedMonth}
</Box>
<Button variant="default" onClick={() => (window.location.hash = next)}>
{t('Forward')}
</Button>
</Group>
</Container>
</Center>
<Footer />
</main>
</>
)
}
export async function getServerSideProps() {
const { workerConfig } = await import('@/uptime.config')
// Only present these values to client
const monitors: MonitorTarget[] = workerConfig.monitors.map((monitor) => ({
id: monitor.id,
name: monitor.name,
})) as MonitorTarget[]
return { props: { monitors } }
}
+91
View File
@@ -0,0 +1,91 @@
import Head from 'next/head'
import { Inter } from 'next/font/google'
import { MonitorTarget } from '@/types/config'
import { maintenances, pageConfig } from '@/uptime.config'
import OverallStatus from '@/components/OverallStatus'
import Header from '@/components/Header'
import MonitorList from '@/components/MonitorList'
import { Center, Text } from '@mantine/core'
import MonitorDetail from '@/components/MonitorDetail'
import Footer from '@/components/Footer'
import { useTranslation } from 'react-i18next'
import { CompactedMonitorStateWrapper, getFromStore } from '@/worker/src/store'
export const runtime = 'experimental-edge'
const inter = Inter({ subsets: ['latin'] })
export default function Home({
compactedStateStr,
monitors,
}: {
compactedStateStr: string
monitors: MonitorTarget[]
tooltip?: string
statusPageLink?: string
}) {
const { t } = useTranslation('common')
let state = new CompactedMonitorStateWrapper(compactedStateStr).uncompact()
// Specify monitorId in URL hash to view a specific monitor (can be used in iframe)
const monitorId = window.location.hash.substring(1)
if (monitorId) {
const monitor = monitors.find((monitor) => monitor.id === monitorId)
if (!monitor || !state) {
return <Text fw={700}>{t('Monitor not found', { id: monitorId })}</Text>
}
return (
<div style={{ maxWidth: '810px' }}>
<MonitorDetail monitor={monitor} state={state} />
</div>
)
}
return (
<>
<Head>
<title>{pageConfig.title}</title>
<link rel="icon" href={pageConfig.favicon ?? '/favicon.png'} />
</Head>
<main className={inter.className}>
<Header />
{state.lastUpdate === 0 ? (
<Center>
<Text fw={700}>{t('Monitor State not defined')}</Text>
</Center>
) : (
<div>
<OverallStatus state={state} monitors={monitors} maintenances={maintenances} />
<MonitorList monitors={monitors} state={state} />
</div>
)}
<Footer />
</main>
</>
)
}
export async function getServerSideProps() {
const { workerConfig } = await import('@/uptime.config')
// Read state as string from storage, to avoid hitting server-side cpu time limit
const compactedStateStr = await getFromStore(process.env as any, 'state')
// Only present these values to client
const monitors = workerConfig.monitors.map((monitor) => {
return {
id: monitor.id,
name: monitor.name,
// @ts-ignore
tooltip: monitor?.tooltip,
// @ts-ignore
statusPageLink: monitor?.statusPageLink,
// @ts-ignore
hideLatencyChart: monitor?.hideLatencyChart,
}
})
return { props: { compactedStateStr, monitors } }
}