maintenance mode (#101)
* feat: move types * feat: add maintenance alert * feat: move to config/base class to have fallback values on missing exports * fix: wrong import for types * Revert "fix: wrong import for types" This reverts commitpull/108/headca2b2e3a68. * Revert "feat: move to config/base class to have fallback values on missing exports" This reverts commit9932ea2590. * fix: imports * chore: formatting * fix: handle filtering for global status * fix: pass props * fix: ignore active maintenances when sending notifications * fix: mark as warning when monitor is in maintenance * fix: title width & warning icon * fix: remove accordeons again * alert config/style changes, worker code improve * update example config * update example config * set monitors to be optional --------- Co-authored-by: lyc8503 <me@lyc8503.net>
parent
8a6cde7240
commit
a6c8eb6e12
|
|
@ -1,4 +1,4 @@
|
|||
import { MonitorState, MonitorTarget } from '@/uptime.types'
|
||||
import { MonitorState, MonitorTarget } from '@/types/config'
|
||||
import { getColor } from '@/util/color'
|
||||
import { Box, Tooltip, Modal } from '@mantine/core'
|
||||
import { useResizeObserver } from '@mantine/hooks'
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
TimeScale,
|
||||
} from 'chart.js'
|
||||
import 'chartjs-adapter-moment'
|
||||
import { MonitorState, MonitorTarget } from '@/uptime.types'
|
||||
import { MonitorState, MonitorTarget } from '@/types/config'
|
||||
import { iataToCountry } from '@/util/iata'
|
||||
|
||||
ChartJS.register(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Container, Group, Text } from '@mantine/core'
|
||||
import classes from '@/styles/Header.module.css'
|
||||
import { pageConfig } from '@/uptime.config'
|
||||
import { PageConfigLink } from '@/types/config'
|
||||
|
||||
export default function Header() {
|
||||
const linkToElement = (link: { label: string; link: string; highlight?: boolean }) => {
|
||||
const linkToElement = (link: PageConfigLink) => {
|
||||
return (
|
||||
<a
|
||||
key={link.label}
|
||||
|
|
@ -38,11 +39,11 @@ export default function Header() {
|
|||
</div>
|
||||
|
||||
<Group gap={5} visibleFrom="sm">
|
||||
{pageConfig.links.map(linkToElement)}
|
||||
{pageConfig.links?.map(linkToElement)}
|
||||
</Group>
|
||||
|
||||
<Group gap={5} hiddenFrom="sm">
|
||||
{pageConfig.links.filter((link) => (link as any).highlight).map(linkToElement)}
|
||||
{pageConfig.links?.filter((link) => link.highlight).map(linkToElement)}
|
||||
</Group>
|
||||
</Container>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import { Alert, List, Text } from '@mantine/core'
|
||||
import { IconAlertTriangle } from '@tabler/icons-react'
|
||||
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
|
||||
|
||||
export default function MaintenanceAlert({
|
||||
maintenance,
|
||||
style,
|
||||
}: {
|
||||
maintenance: Omit<MaintenanceConfig, 'monitors'> & { monitors?: MonitorTarget[] }
|
||||
style?: React.CSSProperties
|
||||
}) {
|
||||
return (
|
||||
<Alert
|
||||
icon={<IconAlertTriangle />}
|
||||
title={maintenance.title || 'Scheduled Maintenance'}
|
||||
color={maintenance.color || 'yellow'}
|
||||
withCloseButton={false}
|
||||
style={{ position: 'relative', margin: '16px auto 0 auto', ...style }}
|
||||
>
|
||||
{/* Date range in top right */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
right: 10,
|
||||
fontSize: '0.85rem',
|
||||
textAlign: 'right',
|
||||
padding: '2px 8px',
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
<b>From:</b> {new Date(maintenance.start).toLocaleString()}
|
||||
<br />
|
||||
<b>To:</b>{' '}
|
||||
{maintenance.end ? new Date(maintenance.end).toLocaleString() : 'Until further notice'}
|
||||
</div>
|
||||
|
||||
<Text>{maintenance.body}</Text>
|
||||
{maintenance.monitors && maintenance.monitors.length > 0 && (
|
||||
<>
|
||||
<Text mt="xs">
|
||||
<b>Affected components:</b>
|
||||
</Text>
|
||||
<List size="sm" withPadding>
|
||||
{maintenance.monitors.map((comp, compIdx) => (
|
||||
<List.Item key={compIdx}>{comp.name}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</>
|
||||
)}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import { Text, Tooltip } from '@mantine/core'
|
||||
import { MonitorState, MonitorTarget } from '@/uptime.types'
|
||||
import { IconAlertCircle, IconCircleCheck } from '@tabler/icons-react'
|
||||
import { MonitorState, MonitorTarget } from '@/types/config'
|
||||
import { IconAlertCircle, IconAlertTriangle, IconCircleCheck } from '@tabler/icons-react'
|
||||
import DetailChart from './DetailChart'
|
||||
import DetailBar from './DetailBar'
|
||||
import { getColor } from '@/util/color'
|
||||
import { maintenances } from '@/uptime.config'
|
||||
|
||||
export default function MonitorDetail({
|
||||
monitor,
|
||||
|
|
@ -25,11 +26,32 @@ export default function MonitorDetail({
|
|||
</>
|
||||
)
|
||||
|
||||
const statusIcon =
|
||||
let statusIcon =
|
||||
state.incident[monitor.id].slice(-1)[0].end === undefined ? (
|
||||
<IconAlertCircle style={{ width: '1.25em', height: '1.25em', color: '#b91c1c' }} />
|
||||
<IconAlertCircle
|
||||
style={{ width: '1.25em', height: '1.25em', color: '#b91c1c', marginRight: '3px' }}
|
||||
/>
|
||||
) : (
|
||||
<IconCircleCheck style={{ width: '1.25em', height: '1.25em', color: '#059669' }} />
|
||||
<IconCircleCheck
|
||||
style={{ width: '1.25em', height: '1.25em', color: '#059669', marginRight: '3px' }}
|
||||
/>
|
||||
)
|
||||
|
||||
// Hide real status icon if monitor is in maintenance
|
||||
const now = new Date()
|
||||
const hasMaintenance = maintenances
|
||||
.filter((m) => now >= new Date(m.start) && (!m.end || now <= new Date(m.end)))
|
||||
.find((maintenance) => maintenance.monitors?.includes(monitor.id))
|
||||
if (hasMaintenance)
|
||||
statusIcon = (
|
||||
<IconAlertTriangle
|
||||
style={{
|
||||
width: '1.25em',
|
||||
height: '1.25em',
|
||||
color: '#fab005',
|
||||
marginRight: '3px',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
let totalTime = Date.now() / 1000 - state.incident[monitor.id][0].start[0]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { MonitorState, MonitorTarget } from '@/uptime.types'
|
||||
import { MonitorState, MonitorTarget } from '@/types/config'
|
||||
import { Accordion, Card, Center, Text } from '@mantine/core'
|
||||
import MonitorDetail from './MonitorDetail'
|
||||
import { pageConfig } from '@/uptime.config'
|
||||
|
|
@ -102,8 +102,8 @@ export default function MonitorList({
|
|||
shadow="sm"
|
||||
padding="lg"
|
||||
radius="md"
|
||||
ml="xl"
|
||||
mr="xl"
|
||||
ml="md"
|
||||
mr="md"
|
||||
mt="xl"
|
||||
withBorder={!groupedMonitor}
|
||||
style={{ width: groupedMonitor ? '897px' : '865px' }}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,32 @@
|
|||
import { Center, Title } from '@mantine/core'
|
||||
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
|
||||
import { Center, Container, Title } from '@mantine/core'
|
||||
import { IconCircleCheck, IconAlertCircle } from '@tabler/icons-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import MaintenanceAlert from './MaintenanceAlert'
|
||||
import { pageConfig } from '@/uptime.config'
|
||||
|
||||
function useWindowVisibility() {
|
||||
const [isVisible, setIsVisible] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
console.log('visibility change', document.visibilityState)
|
||||
setIsVisible(document.visibilityState === 'visible')
|
||||
}
|
||||
|
||||
const handleVisibilityChange = () => setIsVisible(document.visibilityState === 'visible')
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
}
|
||||
return () => document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
}, [])
|
||||
|
||||
return isVisible
|
||||
}
|
||||
|
||||
export default function OverallStatus({
|
||||
state,
|
||||
maintenances,
|
||||
monitors,
|
||||
}: {
|
||||
state: { overallUp: number; overallDown: number; lastUpdate: number }
|
||||
maintenances: MaintenanceConfig[]
|
||||
monitors: MonitorTarget[]
|
||||
}) {
|
||||
let group = pageConfig.group
|
||||
let groupedMonitor = (group && Object.keys(group).length > 0) || false
|
||||
|
||||
let statusString = ''
|
||||
let icon = <IconAlertCircle style={{ width: 64, height: 64, color: '#b91c1c' }} />
|
||||
if (state.overallUp === 0 && state.overallDown === 0) {
|
||||
|
|
@ -47,21 +48,28 @@ export default function OverallStatus({
|
|||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (!isWindowVisible) {
|
||||
return
|
||||
}
|
||||
if (!isWindowVisible) return
|
||||
if (currentTime - state.lastUpdate > 300 && currentTime - openTime > 30) {
|
||||
// trigger a re-fetch
|
||||
window.location.reload()
|
||||
}
|
||||
setCurrentTime(Math.round(Date.now() / 1000))
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
})
|
||||
|
||||
const now = new Date()
|
||||
let filteredMaintenances: (Omit<MaintenanceConfig, 'monitors'> & { monitors?: MonitorTarget[] })[] =
|
||||
maintenances
|
||||
.filter((m) => now >= new Date(m.start) && (!m.end || now <= new Date(m.end)))
|
||||
.map((maintenance) => ({
|
||||
...maintenance,
|
||||
monitors: maintenance.monitors?.map(
|
||||
(monitorId) => monitors.find((mon) => monitorId === mon.id)!
|
||||
),
|
||||
}))
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container size="md" mt="xl">
|
||||
<Center>{icon}</Center>
|
||||
<Title mt="sm" style={{ textAlign: 'center' }} order={1}>
|
||||
{statusString}
|
||||
|
|
@ -72,6 +80,14 @@ export default function OverallStatus({
|
|||
currentTime - state.lastUpdate
|
||||
} sec ago)`}
|
||||
</Title>
|
||||
</>
|
||||
|
||||
{filteredMaintenances.map((maintenance, idx) => (
|
||||
<MaintenanceAlert
|
||||
key={idx}
|
||||
maintenance={maintenance}
|
||||
style={{ maxWidth: groupedMonitor ? '897px' : '865px' }}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { workerConfig } from '@/uptime.config'
|
||||
import { MonitorState } from '@/uptime.types'
|
||||
import { MonitorState } from '@/types/config'
|
||||
import { NextRequest } from 'next/server'
|
||||
|
||||
export const runtime = 'edge'
|
||||
|
|
|
|||
|
|
@ -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/config'
|
||||
import { KVNamespace } from '@cloudflare/workers-types'
|
||||
import { pageConfig, workerConfig } from '@/uptime.config'
|
||||
import { maintenances, pageConfig, workerConfig } from '@/uptime.config'
|
||||
import OverallStatus from '@/components/OverallStatus'
|
||||
import Header from '@/components/Header'
|
||||
import MonitorList from '@/components/MonitorList'
|
||||
|
|
@ -60,7 +60,7 @@ export default function Home({
|
|||
</Center>
|
||||
) : (
|
||||
<div>
|
||||
<OverallStatus state={state} />
|
||||
<OverallStatus state={state} monitors={monitors} maintenances={maintenances} />
|
||||
<MonitorList monitors={monitors} state={state} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
export type PageConfig = {
|
||||
title?: string
|
||||
links?: PageConfigLink[]
|
||||
group?: PageConfigGroup
|
||||
}
|
||||
|
||||
export type MaintenanceConfig = {
|
||||
monitors?: string[]
|
||||
title?: string
|
||||
body: string
|
||||
start: number | string
|
||||
end?: number | string
|
||||
color?: string
|
||||
}
|
||||
|
||||
export type PageConfigGroup = { [key: string]: string[] }
|
||||
|
||||
export type PageConfigLink = {
|
||||
link: string
|
||||
label: string
|
||||
highlight?: boolean
|
||||
}
|
||||
|
||||
export type MonitorTarget = {
|
||||
id: string
|
||||
name: string
|
||||
method: string
|
||||
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 WorkerConfig = {
|
||||
kvWriteCooldownMinutes: number
|
||||
monitors: MonitorTarget[]
|
||||
notification?: Notification
|
||||
callbacks?: Callbacks
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
export type MonitorState = {
|
||||
lastUpdate: number
|
||||
overallUp: number
|
||||
overallDown: number
|
||||
incident: Record<
|
||||
string,
|
||||
{
|
||||
start: number[]
|
||||
end: number | undefined // undefined if it's still open
|
||||
error: string[]
|
||||
}[]
|
||||
>
|
||||
|
||||
latency: Record<
|
||||
string,
|
||||
{
|
||||
recent: {
|
||||
loc: string
|
||||
ping: number
|
||||
time: number
|
||||
}[] // recent 12 hour data, 2 min interval
|
||||
all: {
|
||||
loc: string
|
||||
ping: number
|
||||
time: number
|
||||
}[] // all data in 90 days, 1 hour interval
|
||||
}
|
||||
>
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
const pageConfig = {
|
||||
import { MaintenanceConfig, PageConfig, WorkerConfig } from './types/config'
|
||||
|
||||
const pageConfig: PageConfig = {
|
||||
// Title for your status page
|
||||
title: "lyc8503's Status Page",
|
||||
// Links shown at the header of your status page, could set `highlight` to `true`
|
||||
|
|
@ -16,7 +18,7 @@ const pageConfig = {
|
|||
},
|
||||
}
|
||||
|
||||
const workerConfig = {
|
||||
const workerConfig: WorkerConfig = {
|
||||
// Write KV at most every 3 minutes unless the status changed
|
||||
kvWriteCooldownMinutes: 3,
|
||||
// Enable HTTP Basic auth for status page & API by uncommenting the line below, format `<USERNAME>:<PASSWORD>`
|
||||
|
|
@ -117,5 +119,28 @@ const workerConfig = {
|
|||
},
|
||||
}
|
||||
|
||||
// You can define multiple maintenances here
|
||||
// During maintenance, an alert will be shown at status page
|
||||
// Also, related downtime notifications will be skipped (if any)
|
||||
// Of course, you can leave it empty if you don't need this feature
|
||||
// const maintenances: MaintenanceConfig[] = []
|
||||
const maintenances: MaintenanceConfig[] = [
|
||||
{
|
||||
// [Optional] Monitor IDs to be affected by this maintenance
|
||||
monitors: ['foo_monitor', 'bar_monitor'],
|
||||
// [Optional] default to "Scheduled Maintenance" if not specified
|
||||
title: 'Test Maintenance',
|
||||
// Description of the maintenance, will be shown at status page
|
||||
body: 'This is a test maintenance, server software upgrade',
|
||||
// Start time of the maintenance, in UNIX timestamp or ISO 8601 format
|
||||
start: '2025-04-27T00:00:00+08:00',
|
||||
// [Optional] end time of the maintenance, in UNIX timestamp or ISO 8601 format
|
||||
// if not specified, the maintenance will be considered as on-going
|
||||
end: '2025-04-30T00:00:00+08:00',
|
||||
// [Optional] color of the maintenance alert at status page, default to "yellow"
|
||||
color: 'blue',
|
||||
},
|
||||
]
|
||||
|
||||
// Don't forget this, otherwise compilation fails.
|
||||
export { pageConfig, workerConfig }
|
||||
export { pageConfig, workerConfig, maintenances }
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
type MonitorState = {
|
||||
lastUpdate: number
|
||||
overallUp: number
|
||||
overallDown: number
|
||||
incident: Record<
|
||||
string,
|
||||
{
|
||||
start: number[]
|
||||
end: number | undefined // undefined if it's still open
|
||||
error: string[]
|
||||
}[]
|
||||
>
|
||||
|
||||
latency: Record<
|
||||
string,
|
||||
{
|
||||
recent: {
|
||||
loc: string
|
||||
ping: number
|
||||
time: number
|
||||
}[] // recent 12 hour data, 2 min interval
|
||||
all: {
|
||||
loc: string
|
||||
ping: number
|
||||
time: number
|
||||
}[] // all data in 90 days, 1 hour interval
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
type MonitorTarget = {
|
||||
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
|
||||
responseKeyword?: string
|
||||
responseForbiddenKeyword?: string
|
||||
}
|
||||
|
||||
export type { MonitorState, MonitorTarget }
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { workerConfig } from '../../uptime.config'
|
||||
import { workerConfig, maintenances } from '../../uptime.config'
|
||||
import { formatStatusChangeNotification, getWorkerLocation, notifyWithApprise } from './util'
|
||||
import { MonitorState, MonitorTarget } from '../../uptime.types'
|
||||
import { MonitorState, MonitorTarget } from '../../types/config'
|
||||
import { getStatus } from './monitor'
|
||||
import { DurableObject } from 'cloudflare:workers'
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ export interface Env {
|
|||
REMOTE_CHECKER_DO: DurableObjectNamespace<RemoteChecker>
|
||||
}
|
||||
|
||||
export default {
|
||||
const Worker = {
|
||||
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
|
||||
const workerLocation = (await getWorkerLocation()) || 'ERROR'
|
||||
console.log(`Running scheduled event on ${workerLocation}...`)
|
||||
|
|
@ -23,8 +23,7 @@ export default {
|
|||
reason: string
|
||||
) => {
|
||||
// Skip notification if monitor is in the skip list
|
||||
// @ts-ignore
|
||||
const skipList: string[] = workerConfig.notification?.skipNotificationIds
|
||||
const skipList = workerConfig.notification?.skipNotificationIds
|
||||
if (skipList && skipList.includes(monitor.id)) {
|
||||
console.log(
|
||||
`Skipping notification for ${monitor.name} (${monitor.id} in skipNotificationIds)`
|
||||
|
|
@ -32,6 +31,21 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
// Skip notification if monitor is in maintenance
|
||||
const maintenanceList = maintenances
|
||||
.filter(
|
||||
(m) =>
|
||||
new Date(timeNow * 1000) >= new Date(m.start) &&
|
||||
(!m.end || new Date(timeNow * 1000) <= new Date(m.end))
|
||||
)
|
||||
.map((e) => (e.monitors || []))
|
||||
.flat()
|
||||
|
||||
if (maintenanceList.includes(monitor.id)) {
|
||||
console.log(`Skipping notification for ${monitor.name} (in maintenance)`)
|
||||
return
|
||||
}
|
||||
|
||||
if (workerConfig.notification?.appriseApiServer && workerConfig.notification?.recipientUrl) {
|
||||
const notification = formatStatusChangeNotification(
|
||||
monitor,
|
||||
|
|
@ -159,7 +173,7 @@ export default {
|
|||
}
|
||||
|
||||
console.log('Calling config onStatusChange callback...')
|
||||
await workerConfig.callbacks.onStatusChange(
|
||||
await workerConfig.callbacks?.onStatusChange(
|
||||
env,
|
||||
monitor,
|
||||
true,
|
||||
|
|
@ -230,7 +244,7 @@ export default {
|
|||
|
||||
if (monitorStatusChanged) {
|
||||
console.log('Calling config onStatusChange callback...')
|
||||
await workerConfig.callbacks.onStatusChange(
|
||||
await workerConfig.callbacks?.onStatusChange(
|
||||
env,
|
||||
monitor,
|
||||
false,
|
||||
|
|
@ -246,7 +260,7 @@ export default {
|
|||
|
||||
try {
|
||||
console.log('Calling config onIncident callback...')
|
||||
await workerConfig.callbacks.onIncident(
|
||||
await workerConfig.callbacks?.onIncident(
|
||||
env,
|
||||
monitor,
|
||||
currentIncident.start[0],
|
||||
|
|
@ -323,6 +337,8 @@ export default {
|
|||
},
|
||||
}
|
||||
|
||||
export default Worker
|
||||
|
||||
export class RemoteChecker extends DurableObject {
|
||||
constructor(ctx: DurableObjectState, env: Env) {
|
||||
super(ctx, env)
|
||||
|
|
|
|||
Loading…
Reference in New Issue