diff --git a/README.md b/README.md
index bebe8c4..4edee64 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@ Please refer to [Wiki](https://github.com/lyc8503/UptimeFlare/wiki)
- [x] Remove old incidents
- [x] ~~Known issue~~: `fetch` doesn't support non-standard port (resolved after CF update)
- [x] Compatibility date update
-- [x] Scheduled Maintenance
+- [x] Scheduled Maintenance
- [ ] Update wiki/README and add docs for dev
- [ ] Migration to Terraform Cloudflare provider version 5.x
- [ ] Cloudflare D1 database
diff --git a/components/Footer.tsx b/components/Footer.tsx
new file mode 100644
index 0000000..9153254
--- /dev/null
+++ b/components/Footer.tsx
@@ -0,0 +1,31 @@
+import { Divider, Text } from '@mantine/core'
+
+export default function Footer() {
+ return (
+ <>
+
+
+ Open-source monitoring and status page powered by{' '}
+
+ Uptimeflare
+ {' '}
+ and{' '}
+
+ Cloudflare
+
+ , made with ❤ by{' '}
+
+ lyc8503
+
+ .
+
+ >
+ )
+}
diff --git a/components/Header.tsx b/components/Header.tsx
index 2e6a149..12dd976 100644
--- a/components/Header.tsx
+++ b/components/Header.tsx
@@ -3,13 +3,13 @@ import classes from '@/styles/Header.module.css'
import { pageConfig } from '@/uptime.config'
import { PageConfigLink } from '@/types/config'
-export default function Header() {
- const linkToElement = (link: PageConfigLink) => {
+export default function Header({ style }: { style?: React.CSSProperties }) {
+ const linkToElement = (link: PageConfigLink, i: number) => {
return (
@@ -18,11 +18,16 @@ export default function Header() {
)
}
+ const links = [{ label: 'Incident History', link: '/incidents' }, ...(pageConfig.links || [])]
+
return (
-
+
- {pageConfig.links?.map(linkToElement)}
+ {links?.map(linkToElement)}
- {pageConfig.links?.filter((link) => link.highlight).map(linkToElement)}
+ {links?.filter((link) => link.highlight || link.link.startsWith('/')).map(linkToElement)}
diff --git a/components/MaintenanceAlert.tsx b/components/MaintenanceAlert.tsx
index 5919e0c..c4a84d7 100644
--- a/components/MaintenanceAlert.tsx
+++ b/components/MaintenanceAlert.tsx
@@ -1,4 +1,5 @@
-import { Alert, List, Text, Group, Box } from '@mantine/core'
+import { Alert, List, Text, Group, Box, useMantineTheme } from '@mantine/core'
+import { useMediaQuery } from '@mantine/hooks'
import { IconAlertTriangle } from '@tabler/icons-react'
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
import { pageConfig } from '@/uptime.config'
@@ -12,6 +13,9 @@ export default function MaintenanceAlert({
style?: React.CSSProperties
upcoming?: boolean
}) {
+ const theme = useMantineTheme()
+ const isDesktop = useMediaQuery(`(min-width: ${theme.breakpoints.sm})`)
+
return (
}
@@ -20,7 +24,7 @@ export default function MaintenanceAlert({
? `Upcoming Maintenance: ${maintenance.title || 'Scheduled Maintenance'}`
: maintenance.title || 'Scheduled Maintenance'
}
- color={upcoming ? (pageConfig.maintenances?.upcomingColor ?? 'gray') : maintenance.color || '#f29030'}
+ color={upcoming ? (pageConfig.maintenances?.upcomingColor ?? "gray") : maintenance.color || '#f29030'}
withCloseButton={false}
style={{ margin: '16px auto', ...style }}
>
@@ -34,7 +38,7 @@ export default function MaintenanceAlert({
[`@media (max-width: 600px)`]: { gap: 8 },
}}
>
- {/* Maintenance description and affected components */}
+ {/* Maintenance description and affected components */}
{maintenance.body}
@@ -55,18 +59,18 @@ export default function MaintenanceAlert({
style={{
flex: '0 0 auto',
fontSize: '0.85rem',
- textAlign: 'right',
+ textAlign: isDesktop ? 'right' : 'left',
minWidth: 120,
}}
>
- {upcoming ? 'Scheduled for' : 'From'}: {new Date(maintenance.start).toLocaleString()}
-
- {upcoming ? 'Expected end' : 'To'}:{' '}
- {maintenance.end
- ? new Date(maintenance.end).toLocaleString()
- : 'Until further notice'}
+ {upcoming ? 'Scheduled for' : 'From'}:{' '}{new Date(maintenance.start).toLocaleString()}
+
+ {upcoming ? 'Expected end' : 'To'}:{' '}
+ {maintenance.end
+ ? new Date(maintenance.end).toLocaleString()
+ : 'Until further notice'}
)
-}
+}
\ No newline at end of file
diff --git a/components/MonitorList.tsx b/components/MonitorList.tsx
index fdaf7e9..610caf0 100644
--- a/components/MonitorList.tsx
+++ b/components/MonitorList.tsx
@@ -42,7 +42,9 @@ export default function MonitorList({
// Load expanded groups from localStorage
const savedExpandedGroups = localStorage.getItem('expandedGroups')
- const expandedInitial = savedExpandedGroups ? JSON.parse(savedExpandedGroups) : Object.keys(group || {})
+ const expandedInitial = savedExpandedGroups
+ ? JSON.parse(savedExpandedGroups)
+ : Object.keys(group || {})
const [expandedGroups, setExpandedGroups] = useState(expandedInitial)
useEffect(() => {
localStorage.setItem('expandedGroups', JSON.stringify(expandedGroups))
diff --git a/components/NoIncidents.tsx b/components/NoIncidents.tsx
new file mode 100644
index 0000000..f3b8066
--- /dev/null
+++ b/components/NoIncidents.tsx
@@ -0,0 +1,29 @@
+import { Alert, Text } from '@mantine/core'
+import { IconInfoCircle } from '@tabler/icons-react'
+
+export default function NoIncidentsAlert({ style }: { style?: React.CSSProperties }) {
+ return (
+ }
+ title={
+
+ {'No incidents in this month'}
+
+ }
+ color="gray"
+ withCloseButton={false}
+ style={{
+ position: 'relative',
+ margin: '16px auto 0 auto',
+ ...style,
+ }}
+ >
+ There are no incidents for this month.
+
+ )
+}
diff --git a/components/OverallStatus.tsx b/components/OverallStatus.tsx
index b20b2df..4955038 100644
--- a/components/OverallStatus.tsx
+++ b/components/OverallStatus.tsx
@@ -58,6 +58,7 @@ export default function OverallStatus({
})
const now = new Date()
+
const activeMaintenances: (Omit & { monitors?: MonitorTarget[] })[] = maintenances
.filter((m) => now >= new Date(m.start) && (!m.end || now <= new Date(m.end)))
.map((maintenance) => ({
diff --git a/pages/incidents.tsx b/pages/incidents.tsx
new file mode 100644
index 0000000..98f3fb6
--- /dev/null
+++ b/pages/incidents.tsx
@@ -0,0 +1,131 @@
+import Head from 'next/head'
+
+import { Inter } from 'next/font/google'
+import { MaintenanceConfig, MonitorTarget } from '@/types/config'
+import { maintenances, pageConfig, workerConfig } 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'
+
+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
+): (Omit & { 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) => workerConfig.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() {
+ const [selectedMonitor, setSelectedMonitor] = useState('')
+ const [selectedMonth, setSelectedMonth] = useState(getSelectedMonth())
+
+ useEffect(() => {
+ const onHashChange = () => setSelectedMonth(getSelectedMonth())
+ window.addEventListener('hashchange', onHashChange)
+ return () => window.removeEventListener('hashchange', onHashChange)
+ }, [])
+
+ const filteredIncidents = filterIncidentsByMonth(maintenances, selectedMonth)
+ const monitorFilteredIncidents = selectedMonitor
+ ? filteredIncidents.filter((i) => i.monitors.find((e) => e.id === selectedMonitor))
+ : filteredIncidents
+
+ const { prev, next } = getPrevNextMonth(selectedMonth)
+
+ const monitorOptions = [
+ { value: '', label: 'All' },
+ ...workerConfig.monitors.map((monitor) => ({
+ value: monitor.id,
+ label: monitor.name,
+ })),
+ ]
+
+ return (
+ <>
+
+ {pageConfig.title}
+
+
+
+
+
+
+
+
+
+
+
+ {monitorFilteredIncidents.length === 0 ? (
+
+ ) : (
+ monitorFilteredIncidents.map((incident, i) => (
+
+ ))
+ )}
+
+
+
+
+ {selectedMonth}
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/pages/index.tsx b/pages/index.tsx
index 7a00c3c..be84a96 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -7,8 +7,9 @@ import { maintenances, pageConfig, workerConfig } from '@/uptime.config'
import OverallStatus from '@/components/OverallStatus'
import Header from '@/components/Header'
import MonitorList from '@/components/MonitorList'
-import { Center, Divider, Text } from '@mantine/core'
+import { Center, Text } from '@mantine/core'
import MonitorDetail from '@/components/MonitorDetail'
+import Footer from '@/components/Footer'
export const runtime = 'experimental-edge'
const inter = Inter({ subsets: ['latin'] })
@@ -65,29 +66,7 @@ export default function Home({
)}
-
-
- Open-source monitoring and status page powered by{' '}
-
- Uptimeflare
- {' '}
- and{' '}
-
- Cloudflare
-
- , made with ❤ by{' '}
-
- lyc8503
-
- .
-
+
>
)
diff --git a/styles/Header.module.css b/styles/Header.module.css
index bd038f4..45257cd 100644
--- a/styles/Header.module.css
+++ b/styles/Header.module.css
@@ -1,6 +1,6 @@
.header {
height: rem(56px);
- margin-bottom: rem(120px);
+ margin-bottom: rem(100px);
background-color: var(--mantine-color-body);
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
diff --git a/worker/src/index.ts b/worker/src/index.ts
index f9b4680..0d9d31e 100644
--- a/worker/src/index.ts
+++ b/worker/src/index.ts
@@ -38,7 +38,7 @@ const Worker = {
new Date(timeNow * 1000) >= new Date(m.start) &&
(!m.end || new Date(timeNow * 1000) <= new Date(m.end))
)
- .map((e) => (e.monitors || []))
+ .map((e) => e.monitors || [])
.flat()
if (maintenanceList.includes(monitor.id)) {