[WIP] feat: local deployment, still need more testing, data migration & CI todo
parent
e050065a3c
commit
0258ae1ddc
|
|
@ -0,0 +1,40 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
.terraform/
|
||||||
|
terraform.tfstate*
|
||||||
|
|
||||||
|
/.wrangler
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Stage 1: Build
|
||||||
|
FROM node:lts-bookworm AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY worker/package*.json ./worker/
|
||||||
|
|
||||||
|
RUN npm ci
|
||||||
|
RUN cd worker && npm ci
|
||||||
|
|
||||||
|
# Copy all source files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the Next.js application
|
||||||
|
RUN npx @cloudflare/next-on-pages
|
||||||
|
|
||||||
|
# Stage 2: Production
|
||||||
|
FROM node:lts-bookworm AS production
|
||||||
|
|
||||||
|
# Install bash and curl for runtime
|
||||||
|
RUN apt-get update && apt-get install -y curl cron
|
||||||
|
|
||||||
|
# Copy runtime dependencies from builder stage
|
||||||
|
COPY --from=builder /app/ /app/
|
||||||
|
COPY --from=builder /app/entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
# Expose the Pages port
|
||||||
|
EXPOSE 8788
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Initializing database schema..."
|
||||||
|
cd /app
|
||||||
|
npx wrangler d1 execute uptimeflare_d1 --file=/app/init.sql
|
||||||
|
|
||||||
|
# Start Worker
|
||||||
|
echo "Starting Worker..."
|
||||||
|
cd /app/worker
|
||||||
|
npx wrangler dev --inspector-port 9229 --test-scheduled --persist-to ../.wrangler/state --ip 0.0.0.0 --port 8787 2>&1 > /app/worker.log &
|
||||||
|
|
||||||
|
# Start Pages
|
||||||
|
echo "Starting Pages..."
|
||||||
|
cd /app
|
||||||
|
npx wrangler pages dev .vercel/output/static --inspector-port 9230 --ip 0.0.0.0 --port 8788 2>&1 > /app/pages.log &
|
||||||
|
|
||||||
|
# CRON Loop
|
||||||
|
echo "Starting CRON loop..."
|
||||||
|
touch /app/scheduled.log
|
||||||
|
echo "* * * * * /usr/bin/curl -m 60 -s 'http://127.0.0.1:8787/__scheduled' >> /app/scheduled.log 2>&1" | crontab -
|
||||||
|
cron
|
||||||
|
|
||||||
|
exec tail -f /app/worker.log /app/pages.log /app/scheduled.log
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
"No incidents in this month": "No incidents in this month",
|
"No incidents in this month": "No incidents in this month",
|
||||||
"There are no incidents for this month": "There are no incidents for this month.",
|
"There are no incidents for this month": "There are no incidents for this month.",
|
||||||
"Monitor not found": "Monitor with id {{id}} not found!",
|
"Monitor not found": "Monitor with id {{id}} not found!",
|
||||||
"Monitor State not defined": "Monitor State is not defined now, please check your worker's status and KV binding!",
|
"Monitor State not defined": "Monitor State is not defined now, please check your worker's status and binding!",
|
||||||
"All": "All",
|
"All": "All",
|
||||||
"Select monitor": "Select monitor",
|
"Select monitor": "Select monitor",
|
||||||
"Backwards": "← Backwards",
|
"Backwards": "← Backwards",
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
"No incidents in this month": "本月无故障",
|
"No incidents in this month": "本月无故障",
|
||||||
"There are no incidents for this month": "本月没有发生任何故障。",
|
"There are no incidents for this month": "本月没有发生任何故障。",
|
||||||
"Monitor not found": "未找到 ID 为 {{id}} 的监控!",
|
"Monitor not found": "未找到 ID 为 {{id}} 的监控!",
|
||||||
"Monitor State not defined": "监控状态未定义,请检查您的 Worker 状态和 KV 绑定!",
|
"Monitor State not defined": "监控状态未定义,请检查您的 Worker 状态和绑定!",
|
||||||
"All": "全部",
|
"All": "全部",
|
||||||
"Select monitor": "选择监控",
|
"Select monitor": "选择监控",
|
||||||
"Backwards": "← 上一月",
|
"Backwards": "← 上一月",
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"preview": "npx @cloudflare/next-on-pages && wrangler pages dev .vercel/output/static --compatibility-flag nodejs_compat --kv UPTIMEFLARE_STATE",
|
"preview": "npx @cloudflare/next-on-pages && wrangler pages dev .vercel/output/static",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { workerConfig } from '@/uptime.config'
|
|
||||||
import { MonitorState } from '@/types/config'
|
|
||||||
import { NextRequest } from 'next/server'
|
import { NextRequest } from 'next/server'
|
||||||
|
import { CompactedMonitorStateWrapper, getFromStore } from '@/worker/src/store'
|
||||||
|
|
||||||
export const runtime = 'edge'
|
export const runtime = 'edge'
|
||||||
|
|
||||||
|
|
@ -31,8 +30,7 @@ export default async function handler(req: NextRequest): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const url = new URL(req.url)
|
const url = new URL(req.url)
|
||||||
|
|
||||||
const defaultMonitorId = workerConfig.monitors[0]?.id
|
const monitorId = url.searchParams.get('id')
|
||||||
const monitorId = url.searchParams.get('id') ?? defaultMonitorId
|
|
||||||
const label = url.searchParams.get('label') ?? monitorId ?? 'UptimeFlare'
|
const label = url.searchParams.get('label') ?? monitorId ?? 'UptimeFlare'
|
||||||
|
|
||||||
const upMsg = url.searchParams.get('up') ?? 'UP'
|
const upMsg = url.searchParams.get('up') ?? 'UP'
|
||||||
|
|
@ -47,31 +45,12 @@ export default async function handler(req: NextRequest): Promise<Response> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const { UPTIMEFLARE_STATE } = process.env as unknown as {
|
const compactedState = new CompactedMonitorStateWrapper(
|
||||||
UPTIMEFLARE_STATE: KVNamespace
|
await getFromStore(process.env as any, 'state')
|
||||||
}
|
)
|
||||||
|
|
||||||
const stateStr = await UPTIMEFLARE_STATE?.get('state')
|
const lastIncident = compactedState.getIncident(monitorId, compactedState.incidentLen(monitorId) - 1)
|
||||||
if (!stateStr) {
|
const isUp = lastIncident?.end !== null
|
||||||
return new Response(JSON.stringify(errorBadge(label, 'unavailable')), {
|
|
||||||
headers: jsonHeaders,
|
|
||||||
status: 503,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = JSON.parse(stateStr) as MonitorState
|
|
||||||
const monitorIncidentHistory = state.incident?.[monitorId]
|
|
||||||
const hasLatencyData = Boolean(state.latency?.[monitorId]?.length)
|
|
||||||
|
|
||||||
if (!monitorIncidentHistory || monitorIncidentHistory.length === 0 || !hasLatencyData) {
|
|
||||||
return new Response(JSON.stringify(errorBadge(label, 'unknown')), {
|
|
||||||
headers: jsonHeaders,
|
|
||||||
status: 404,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestIncident = monitorIncidentHistory.slice(-1)[0]
|
|
||||||
const isUp = latestIncident.end !== undefined
|
|
||||||
|
|
||||||
const badge: BadgePayload = {
|
const badge: BadgePayload = {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,49 @@
|
||||||
import { workerConfig } from '@/uptime.config'
|
import { workerConfig } from '@/uptime.config'
|
||||||
import { MonitorState } from '@/types/config'
|
|
||||||
import { NextRequest } from 'next/server'
|
import { NextRequest } from 'next/server'
|
||||||
|
import { CompactedMonitorStateWrapper, getFromStore } from '@/worker/src/store'
|
||||||
|
|
||||||
export const runtime = 'edge'
|
export const runtime = 'edge'
|
||||||
|
|
||||||
export default async function handler(req: NextRequest): Promise<Response> {
|
const headers = {
|
||||||
const { UPTIMEFLARE_STATE } = process.env as unknown as {
|
'Content-Type': 'application/json',
|
||||||
UPTIMEFLARE_STATE: KVNamespace
|
'Access-Control-Allow-Origin': '*',
|
||||||
}
|
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type',
|
||||||
|
}
|
||||||
|
|
||||||
const stateStr = await UPTIMEFLARE_STATE?.get('state')
|
export default async function handler(req: NextRequest): Promise<Response> {
|
||||||
if (!stateStr) {
|
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' }), {
|
return new Response(JSON.stringify({ error: 'No data available' }), {
|
||||||
status: 500,
|
status: 500,
|
||||||
headers: {
|
headers
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
||||||
'Access-Control-Allow-Headers': 'Content-Type',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const state = JSON.parse(stateStr) as unknown as MonitorState
|
|
||||||
|
|
||||||
let monitors: any = {}
|
let monitors: any = {}
|
||||||
|
|
||||||
for (let monitor of workerConfig.monitors) {
|
for (let monitor of workerConfig.monitors) {
|
||||||
const isUp = state.incident[monitor.id].slice(-1)[0].end !== undefined
|
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] = {
|
monitors[monitor.id] = {
|
||||||
up: isUp,
|
up: isUp,
|
||||||
latency: state.latency[monitor.id].slice(-1)[0].ping,
|
latency: latency.ping,
|
||||||
location: state.latency[monitor.id].slice(-1)[0].loc,
|
location: latency.loc,
|
||||||
message: isUp ? 'OK' : state.incident[monitor.id].slice(-1)[0].error.slice(-1)[0],
|
message: isUp ? 'OK' : lastIncident?.error[lastIncident.error.length - 1],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ret = {
|
let ret = {
|
||||||
up: state.overallUp,
|
up: compactedState.data.overallUp,
|
||||||
down: state.overallDown,
|
down: compactedState.data.overallDown,
|
||||||
updatedAt: state.lastUpdate,
|
updatedAt: compactedState.data.lastUpdate,
|
||||||
monitors: monitors,
|
monitors: monitors,
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(JSON.stringify(ret), {
|
return new Response(JSON.stringify(ret), {
|
||||||
headers: {
|
headers
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
||||||
'Access-Control-Allow-Headers': 'Content-Type',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
|
|
||||||
import { Inter } from 'next/font/google'
|
import { Inter } from 'next/font/google'
|
||||||
import { MonitorState, MonitorTarget } from '@/types/config'
|
import { MonitorTarget } from '@/types/config'
|
||||||
import { KVNamespace } from '@cloudflare/workers-types'
|
|
||||||
import { maintenances, pageConfig, workerConfig } from '@/uptime.config'
|
import { maintenances, pageConfig, workerConfig } from '@/uptime.config'
|
||||||
import OverallStatus from '@/components/OverallStatus'
|
import OverallStatus from '@/components/OverallStatus'
|
||||||
import Header from '@/components/Header'
|
import Header from '@/components/Header'
|
||||||
|
|
@ -11,25 +10,22 @@ import { Center, Text } from '@mantine/core'
|
||||||
import MonitorDetail from '@/components/MonitorDetail'
|
import MonitorDetail from '@/components/MonitorDetail'
|
||||||
import Footer from '@/components/Footer'
|
import Footer from '@/components/Footer'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { getFromStore } from '@/worker/src/store'
|
import { CompactedMonitorStateWrapper, getFromStore } from '@/worker/src/store'
|
||||||
|
|
||||||
export const runtime = 'experimental-edge'
|
export const runtime = 'experimental-edge'
|
||||||
const inter = Inter({ subsets: ['latin'] })
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
export default function Home({
|
export default function Home({
|
||||||
state: stateStr,
|
compactedStateStr,
|
||||||
monitors,
|
monitors,
|
||||||
}: {
|
}: {
|
||||||
state: string
|
compactedStateStr: string
|
||||||
monitors: MonitorTarget[]
|
monitors: MonitorTarget[]
|
||||||
tooltip?: string
|
tooltip?: string
|
||||||
statusPageLink?: string
|
statusPageLink?: string
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('common')
|
const { t } = useTranslation('common')
|
||||||
let state
|
let state = new CompactedMonitorStateWrapper(compactedStateStr).uncompact()
|
||||||
if (stateStr !== undefined) {
|
|
||||||
state = JSON.parse(stateStr) as MonitorState
|
|
||||||
}
|
|
||||||
|
|
||||||
// Specify monitorId in URL hash to view a specific monitor (can be used in iframe)
|
// 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)
|
||||||
|
|
@ -55,7 +51,7 @@ export default function Home({
|
||||||
<main className={inter.className}>
|
<main className={inter.className}>
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
{state == undefined ? (
|
{state.lastUpdate === 0 ? (
|
||||||
<Center>
|
<Center>
|
||||||
<Text fw={700}>{t('Monitor State not defined')}</Text>
|
<Text fw={700}>{t('Monitor State not defined')}</Text>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
@ -73,8 +69,8 @@ export default function Home({
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerSideProps() {
|
export async function getServerSideProps() {
|
||||||
// Read state as string from KV, to avoid hitting server-side cpu time limit
|
// Read state as string from storage, to avoid hitting server-side cpu time limit
|
||||||
const state = (await getFromStore(process.env as any, 'state')) as unknown as MonitorState
|
const compactedStateStr = await getFromStore(process.env as any, 'state')
|
||||||
|
|
||||||
// Only present these values to client
|
// Only present these values to client
|
||||||
const monitors = workerConfig.monitors.map((monitor) => {
|
const monitors = workerConfig.monitors.map((monitor) => {
|
||||||
|
|
@ -90,5 +86,5 @@ export async function getServerSideProps() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return { props: { state, monitors } }
|
return { props: { compactedStateStr, monitors } }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -4,12 +4,12 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "wrangler deploy",
|
"deploy": "wrangler deploy",
|
||||||
"dev": "wrangler dev --config wrangler-dev.toml --test-scheduled --persist-to ../.wrangler/state",
|
"dev": "wrangler dev --test-scheduled --persist-to ../.wrangler/state",
|
||||||
"start": "wrangler dev"
|
"start": "wrangler dev"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20250410.0",
|
"@cloudflare/workers-types": "^4.20250410.0",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"wrangler": "^4.10.0"
|
"wrangler": "^4.54.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Env } from '.'
|
import { Env } from '.'
|
||||||
import { IncidentRecord, LatencyRecord, MonitorStateCompacted } from '../../types/config'
|
import { IncidentRecord, LatencyRecord, MonitorState, MonitorStateCompacted } from '../../types/config'
|
||||||
|
|
||||||
export async function getFromStore(env: Env, key: string): Promise<string | null> {
|
export async function getFromStore(env: Env, key: string): Promise<string | null> {
|
||||||
const stmt = env.UPTIMEFLARE_D1.prepare('SELECT value FROM uptimeflare WHERE key = ?')
|
const stmt = env.UPTIMEFLARE_D1.prepare('SELECT value FROM uptimeflare WHERE key = ?')
|
||||||
|
|
@ -36,6 +36,64 @@ export class CompactedMonitorStateWrapper {
|
||||||
return JSON.stringify(this.data)
|
return JSON.stringify(this.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't use this method at server-side
|
||||||
|
uncompact(): MonitorState {
|
||||||
|
let state: MonitorState = {
|
||||||
|
lastUpdate: this.data.lastUpdate,
|
||||||
|
overallUp: this.data.overallUp,
|
||||||
|
overallDown: this.data.overallDown,
|
||||||
|
incident: {},
|
||||||
|
latency: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(this.data.incident).forEach((monitorId) => {
|
||||||
|
state.incident[monitorId] = []
|
||||||
|
const incidents = this.data.incident[monitorId]
|
||||||
|
|
||||||
|
if (incidents.start.length !== incidents.end.length || incidents.start.length !== incidents.error.length) {
|
||||||
|
throw new Error('Inconsistent incident data lengths, please report an issue at https://github.com/lyc8503/UptimeFlare')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < incidents.start.length; i++) {
|
||||||
|
state.incident[monitorId].push({
|
||||||
|
start: incidents.start[i],
|
||||||
|
end: incidents.end[i],
|
||||||
|
error: incidents.error[i],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.keys(this.data.latency).forEach((monitorId) => {
|
||||||
|
state.latency[monitorId] = []
|
||||||
|
const latencies = this.data.latency[monitorId]
|
||||||
|
const locUncompacted: string[] = []
|
||||||
|
latencies.loc.c.forEach((count, index) => {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
locUncompacted.push(latencies.loc.v[index])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
const timeArr = new Uint32Array(Uint8Array.fromHex(latencies.time).buffer)
|
||||||
|
// @ts-expect-error
|
||||||
|
const pingArr = new Uint16Array(Uint8Array.fromHex(latencies.ping).buffer)
|
||||||
|
|
||||||
|
if (timeArr.length !== pingArr.length || timeArr.length !== locUncompacted.length) {
|
||||||
|
throw new Error('Inconsistent latency data lengths, please report an issue at https://github.com/lyc8503/UptimeFlare.')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < timeArr.length; i++) {
|
||||||
|
state.latency[monitorId].push({
|
||||||
|
time: timeArr[i],
|
||||||
|
ping: pingArr[i],
|
||||||
|
loc: locUncompacted[i],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
incidentLen(monitorId: string): number {
|
incidentLen(monitorId: string): number {
|
||||||
const incidents = this.data.incident[monitorId]
|
const incidents = this.data.incident[monitorId]
|
||||||
if (!incidents) return 0
|
if (!incidents) return 0
|
||||||
|
|
@ -140,6 +198,18 @@ export class CompactedMonitorStateWrapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLastLatency(monitorId: string): LatencyRecord {
|
||||||
|
let latencies = this.data.latency[monitorId]
|
||||||
|
|
||||||
|
return {
|
||||||
|
// @ts-expect-error
|
||||||
|
time: new Uint32Array(Uint8Array.fromHex(latencies.time.slice(-8)).buffer)[0],
|
||||||
|
// @ts-expect-error
|
||||||
|
ping: new Uint16Array(Uint8Array.fromHex(latencies.ping.slice(-4)).buffer)[0],
|
||||||
|
loc: latencies.loc.v[latencies.loc.v.length - 1],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
unshiftLatency(monitorId: string) {
|
unshiftLatency(monitorId: string) {
|
||||||
let latencies = this.data.latency[monitorId]
|
let latencies = this.data.latency[monitorId]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
name = "uptimeflare_worker"
|
|
||||||
main = "src/index.ts"
|
|
||||||
compatibility_date = "2025-04-02"
|
|
||||||
compatibility_flags = [ "nodejs_compat" ]
|
|
||||||
|
|
||||||
[[d1_databases]]
|
|
||||||
binding = "UPTIMEFLARE_D1"
|
|
||||||
database_name = "uptimeflare_d1"
|
|
||||||
database_id = "00000000-0000-0000-0000-000000000000"
|
|
||||||
|
|
@ -3,3 +3,7 @@ main = "src/index.ts"
|
||||||
compatibility_date = "2025-04-02"
|
compatibility_date = "2025-04-02"
|
||||||
compatibility_flags = [ "nodejs_compat" ]
|
compatibility_flags = [ "nodejs_compat" ]
|
||||||
|
|
||||||
|
[[d1_databases]]
|
||||||
|
binding = "UPTIMEFLARE_D1"
|
||||||
|
database_name = "uptimeflare_d1"
|
||||||
|
database_id = "00000000-0000-0000-0000-000000000000"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue