mirror of
https://github.com/ChenQihan666/Lolia-Nodes-Status-Pages.git
synced 2026-08-14 07:52:34 +08:00
Initial commit
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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 } }
|
||||
}
|
||||
@@ -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 } }
|
||||
}
|
||||
Reference in New Issue
Block a user