Compare commits

..

1 Commits

Author SHA1 Message Date
Adam Janiš 3898ffcbc7 d1 init 2022-07-11 09:48:07 +01:00
16 changed files with 3349 additions and 2215 deletions
+3 -2
View File
@@ -26,8 +26,9 @@ jobs:
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
preCommands: |
wrangler kv:namespace create KV_STATUS_PAGE || true
export KV_NAMESPACE_ID=$(npx @cloudflare/wrangler@1 kv:namespace list 2> >(tee stderr.log >&2) | head -1 | node -pe "JSON.parse(fs.readFileSync('/dev/stdin').toString()).find(kv => kv.title.includes('KV_STATUS_PAGE')).id")
wrangler kv:namespace create KV_STATUS_PAGE
apt-get update && apt-get install -y jq
export KV_NAMESPACE_ID=$(wrangler kv:namespace list | jq -c 'map(select(.title | contains("KV_STATUS_PAGE")))' | jq -r ".[0].id")
echo "[env.production]" >> wrangler.toml
echo "kv_namespaces = [{binding=\"KV_STATUS_PAGE\", id=\"${KV_NAMESPACE_ID}\"}]" >> wrangler.toml
[ -z "$SECRET_SLACK_WEBHOOK_URL" ] && echo "Secret SECRET_SLACK_WEBHOOK_URL not set, creating dummy one..." && SECRET_SLACK_WEBHOOK_URL="default-gh-action-secret" || true
+1 -25
View File
@@ -1,5 +1,5 @@
settings:
title: 'Status Page'
title: 'Status Page powered by Workers and D1'
url: 'https://status-page.eidam.dev' # used for Slack messages
logo: logo-192x192.png # image in ./public/ folder
daysInHistogram: 90 # number of days you want to display in histogram
@@ -13,27 +13,3 @@ settings:
dayInHistogramNoData: 'No data'
dayInHistogramOperational: 'All good'
dayInHistogramNotOperational: ' incident(s)' # xx incident(s) recorded
monitors:
- id: workers-cloudflare-com # unique identifier
name: workers.cloudflare.com
description: 'You write code. They handle the rest.' # default=empty
url: 'https://workers.cloudflare.com/' # URL to fetch
method: GET # default=GET
expectStatus: 200 # operational status, default=200
followRedirect: false # should fetch follow redirects, default=false
linkable: false # allows the title to be a link, default=true
- id: www-cloudflare-com
name: www.cloudflare.com
description: 'Built for anything connected to the Internet.'
url: 'https://www.cloudflare.com'
method: GET
expectStatus: 200
linkable: true # allows the title to be a link, default=true
- id: blog-cloudflare-com
name: The Cloudflare Blog
url: 'https://blog.cloudflare.com/'
method: GET
expectStatus: 200
+1 -1
View File
@@ -5,7 +5,7 @@ module.exports = {
type: 'json',
use: 'yaml-loader',
})
return config
},
}
+9 -5
View File
@@ -9,22 +9,26 @@
"dev": "flareact dev",
"build": "yarn css && flareact build",
"deploy": "yarn build && flareact publish",
"kv-gc": "node ./src/cli/gcMonitors.js",
"format": "prettier --write '**/*.{js,css,json,md}'",
"css": "postcss public/tailwind.css -o public/style.css"
"css": "postcss public/tailwind.css -o public/style.css",
"postinstall": "patch-package"
},
"dependencies": {
"flareact": "^0.10.0",
"@cloudflare/d1": "^1.4.1",
"flareact": "^1.5.0",
"laco": "^1.2.1",
"laco-react": "^1.1.0",
"react": "^17.0.1",
"react-dom": "^17.0.1"
"react-dom": "^17.0.1",
"wrangler": "^0.0.0-d35c69f"
},
"devDependencies": {
"autoprefixer": "^10.0.2",
"node-fetch": "^2.6.1",
"postcss": "^8.2.13",
"patch-package": "^6.4.7",
"postcss": "^8.2.10",
"postcss-cli": "^8.3.0",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.2.0",
"tailwindcss": "^2.0.1",
"yaml-loader": "^0.6.0"
+1 -1
View File
@@ -2,5 +2,5 @@ import { processCronTrigger } from '../../src/functions/cronTrigger'
export default async (event) => {
// used only for local debugging
//return processCronTrigger(event)
return processCronTrigger(event)
}
+30 -29
View File
@@ -2,45 +2,29 @@ import { Store } from 'laco'
import { useStore } from 'laco-react'
import Head from 'flareact/head'
import { getKVMonitors, useKeyPress } from '../src/functions/helpers'
import { loadData, useKeyPress } from '../src/functions/helpers'
import config from '../config.yaml'
import MonitorCard from '../src/components/monitorCard'
import MonitorFilter from '../src/components/monitorFilter'
import MonitorStatusHeader from '../src/components/monitorStatusHeader'
import ThemeSwitcher from '../src/components/themeSwitcher'
const MonitorStore = new Store({
monitors: config.monitors,
visible: config.monitors,
activeFilter: false,
})
const filterByTerm = (term) =>
MonitorStore.set((state) => ({
visible: state.monitors.filter((monitor) =>
monitor.name.toLowerCase().includes(term),
),
}))
export async function getEdgeProps() {
// get KV data
const kvMonitors = await getKVMonitors()
const { monitors, checks } = await loadData()
console.log(monitors, checks)
return {
props: {
config,
kvMonitors: kvMonitors ? kvMonitors.monitors : {},
kvMonitorsLastUpdate: kvMonitors ? kvMonitors.lastUpdate : {},
monitors: monitors || {},
checks,
},
// Revalidate these props once every x seconds
revalidate: 5,
}
}
export default function Index({ config, kvMonitors, kvMonitorsLastUpdate }) {
const state = useStore(MonitorStore)
const slash = useKeyPress('/')
export default function Index({ config, monitors, checks }) {
return (
<div className="min-h-screen">
<Head>
@@ -75,21 +59,38 @@ export default function Index({ config, kvMonitors, kvMonitorsLastUpdate }) {
</div>
<div className="flex flex-row items-center">
{typeof window !== 'undefined' && <ThemeSwitcher />}
<MonitorFilter active={slash} callback={filterByTerm} />
</div>
</div>
<MonitorStatusHeader kvMonitorsLastUpdate={kvMonitorsLastUpdate} />
{state.visible.map((monitor, key) => {
<MonitorStatusHeader monitors={monitors} />
{monitors.map((monitor, key) => {
return (
<MonitorCard
key={key}
monitor={monitor}
data={kvMonitors[monitor.id]}
checks={checks.filter(x => x.monitor_id === monitor.id )}
/>
)
})}
<div className="flex flex-row justify-between mt-4 text-sm">
<div>
Powered by{' '}
<a href="https://workers.cloudflare.com/" target="_blank">
Cloudflare Workers{' '}
</a>
&{' '}
<a href="https://flareact.com/" target="_blank">
Flareact{' '}
</a>
</div>
<div>
<a
href="https://github.com/eidam/cf-workers-status-page"
target="_blank"
>
Get Your Status Page
</a>
</div>
</div>
</div>
</div>
)
+101
View File
@@ -0,0 +1,101 @@
diff --git a/node_modules/flareact/configs/webpack.worker.config.js b/node_modules/flareact/configs/webpack.worker.config.js
index 5b9f088..3bfb62f 100644
--- a/node_modules/flareact/configs/webpack.worker.config.js
+++ b/node_modules/flareact/configs/webpack.worker.config.js
@@ -26,6 +26,10 @@ module.exports = function (env, argv) {
...baseConfig({ dev, isServer }),
target: "webworker",
entry: path.resolve(projectDir, "./index"),
+ output: {
+ path: path.resolve(projectDir, "./worker"),
+ filename:'script.js'
+ }
};
config.plugins.push(
diff --git a/node_modules/flareact/src/bin/flareact.js b/node_modules/flareact/src/bin/flareact.js
index ed2ea89..bb53cf0 100755
--- a/node_modules/flareact/src/bin/flareact.js
+++ b/node_modules/flareact/src/bin/flareact.js
@@ -17,20 +17,22 @@ dotenv.config();
const yargs = require("yargs");
let rootPath = "";
-let webpackConfigPath =
+let webpackClientConfigPath =
"node_modules/flareact/configs/webpack.client.config.js";
+let webpackWorkerConfigPath =
+ "node_modules/flareact/configs/webpack.worker.config.js";
-let isWebpackConfigFound = () => fs.existsSync(rootPath + webpackConfigPath);
+let isWebpackClientConfigFound = () => fs.existsSync(rootPath + webpackClientConfigPath);
for (let i = 0; i < 3; i++) {
- if (isWebpackConfigFound()) {
+ if (isWebpackClientConfigFound()) {
break;
}
rootPath += "../";
}
-if (isWebpackConfigFound()) {
- webpackConfigPath = rootPath + webpackConfigPath;
+if (isWebpackClientConfigFound()) {
+ webpackClientConfigPath = rootPath + webpackClientConfigPath;
} else {
const firstLine =
"⚠ Cannot find node_modules/flareact/configs/webpack.client.config.js.";
@@ -73,13 +75,13 @@ if (argv._.includes("dev")) {
concurrently(
[
{
- command: "wrangler dev",
- name: "worker",
- env: { WORKER_DEV: true, IS_WORKER: true },
+ command: `webpack --config ${webpackWorkerConfigPath} --mode production && webpack --config ${webpackClientConfigPath} --mode production && npx wrangler@d1 dev --local`,
+ name: "build",
+ env: { NODE_ENV: "development", WORKER_DEV: true, IS_WORKER: true },
},
{
- command: `webpack-dev-server --config ${webpackConfigPath} --mode development --port ${argv.port}`,
- name: "client",
+ command: `webpack-dev-server --config ${webpackClientConfigPath} --mode development --port ${argv.port}`,
+ name: "dev-server",
env: { NODE_ENV: "development" },
},
],
@@ -101,7 +103,7 @@ if (argv._.includes("publish")) {
console.log(`Publishing your Flareact project to ${destination}...`);
- let wranglerPublish = `wrangler publish`;
+ let wranglerPublish = `npx wrangler@d1 publish`;
if (argv.env) {
wranglerPublish += ` --env ${argv.env}`;
@@ -110,10 +112,10 @@ if (argv._.includes("publish")) {
concurrently(
[
{
- command: `webpack --config ${webpackConfigPath} --out ./out --mode production && ${wranglerPublish}`,
- name: "publish",
- env: { NODE_ENV: "production", IS_WORKER: true },
- },
+ command: `webpack --config ${webpackClientConfigPath} --mode production && webpack --config ${webpackWorkerConfigPath} --mode production && ${wranglerPublish}`,
+ name: "build",
+ env: { NODE_ENV: "production",IS_WORKER: true },
+ }
],
{
prefix: "name",
@@ -134,8 +136,8 @@ if (argv._.includes("build")) {
concurrently(
[
{
- command: `webpack --config ${webpackConfigPath} --out ./out --mode production`,
- name: "publish",
+ command: `webpack --config ${webpackWorkerConfigPath} --mode production && webpack --config ${webpackClientConfigPath} --mode production`,
+ name: "build",
env: { NODE_ENV: "production" },
},
],
+38
View File
@@ -0,0 +1,38 @@
/* npx wrangler@d1 d1 execute <database-name> --file=./schema.sql */
CREATE TABLE IF NOT EXISTS monitors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url TEXT NOT NULL,
operational INTEGER DEFAULT 0,
description TEXT DEFAULT NULL,
method TEXT DEFAULT "GET",
expect_status INTEGER DEFAULT 200,
follow_redirect INTEGER DEFAULT 1,
linkable INTEGER DEFAULT 0,
operational INTEGER DEFAULT 0,
last_updated TEXT DEFAULT CURRENT_TIMESTAMP
);
/*
INSERT INTO monitors (name, url)
VALUES
("workers.cloudflare.com", "https://www.cloudflare.com"),
("www.cloudflare.com", "https://www.cloudflare.com"),
("The Cloudflare Blog", "https://blog.cloudflare.com"),
("Cloudflare community", "https://cloudflare.community");
*/
CREATE TABLE IF NOT EXISTS monitors_checks (
monitor_id INTEGER NOT NULL,
location TEXT NOT NULL,
res_ms INT NOT NULL,
operational INTEGER NOT NULL,
date TEXT DEFAULT CURRENT_DATE,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_monitors
FOREIGN KEY (monitor_id)
REFERENCES monitors(id)
ON DELETE CASCADE
);
+9 -6
View File
@@ -1,6 +1,7 @@
import config from '../../config.yaml'
import MonitorStatusLabel from './monitorStatusLabel'
import MonitorHistogram from './monitorHistogram'
import { locations } from '../functions/locations'
const infoIcon = (
<svg
@@ -17,7 +18,9 @@ const infoIcon = (
</svg>
)
export default function MonitorCard({ key, monitor, data }) {
export default function MonitorCard({ key, monitor, checks }) {
console.log(checks)
return (
<div key={key} className="card">
<div className="flex flex-row justify-between items-center mb-2">
@@ -30,7 +33,7 @@ export default function MonitorCard({ key, monitor, data }) {
</div>
</div>
)}
{(monitor.linkable === true || monitor.linkable === undefined) ?
{monitor.linkable ?
(
<a href={monitor.url} target="_blank">
<div className="text-xl">{monitor.name}</div>
@@ -45,14 +48,14 @@ export default function MonitorCard({ key, monitor, data }) {
}
</div>
<MonitorStatusLabel kvMonitor={data} />
<MonitorStatusLabel monitor={monitor} />
</div>
<MonitorHistogram monitorId={monitor.id} kvMonitor={data} />
{/*<MonitorHistogram monitorId={monitor.id} kvMonitor={data} />*/}
<div className="flex flex-row justify-between items-center text-gray-400 text-sm">
<div>{config.settings.daysInHistogram} days ago</div>
<div>Today</div>
<div>&nbsp;</div>
<div>{checks.map(x => `${locations[x.location] || x.location}: ${Math.floor(x.avg_res)}ms avg - ${x.count} check(s)`).join(" | ")}</div>
</div>
</div>
)
+11 -6
View File
@@ -8,25 +8,30 @@ const classes = {
'bg-yellow-200 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-200 border-yellow-300 dark:border-yellow-600',
}
export default function MonitorStatusHeader({ kvMonitorsLastUpdate }) {
export default function MonitorStatusHeader({ monitors }) {
let color = 'green'
let text = config.settings.allmonitorsOperational
if (!kvMonitorsLastUpdate.allOperational) {
if (monitors.find(x => !x.operational)) {
color = 'yellow'
text = config.settings.notAllmonitorsOperational
}
const updates = monitors.map(m => {
return new Date(m.last_updated).getTime();
});
const lastUpdated = Math.max(...updates);
return (
<div className={`card mb-4 font-semibold ${classes[color]}`}>
<div className="flex flex-row justify-between items-center">
<div>{text}</div>
{kvMonitorsLastUpdate.time && typeof window !== 'undefined' && (
{lastUpdated && typeof window !== 'undefined' && (
<div className="text-xs font-light">
checked{' '}
{Math.round((Date.now() - kvMonitorsLastUpdate.time) / 1000)} sec
ago (from{' '}
{locations[kvMonitorsLastUpdate.loc] || kvMonitorsLastUpdate.loc})
{Math.round((Date.now() - lastUpdated) / 1000)} sec
ago
</div>
)}
</div>
+3 -3
View File
@@ -7,12 +7,12 @@ const classes = {
'bg-yellow-200 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-200',
}
export default function MonitorStatusLabel({ kvMonitor }) {
export default function MonitorStatusLabel({ monitor }) {
let color = 'gray'
let text = 'No data'
if (typeof kvMonitor !== 'undefined') {
if (kvMonitor.lastCheck.operational) {
if (typeof monitor !== 'undefined') {
if (monitor.operational) {
color = 'green'
text = config.settings.monitorLabelOperational
} else {
+122
View File
@@ -0,0 +1,122 @@
export class Database {
binding;
constructor(binding) {
this.binding = binding;
}
prepare(query) {
return new PreparedStatement(this, query);
}
async dump() {
const response = await this.binding.fetch("/dump", {
method: "POST",
headers: {
"content-type": "application/json",
},
});
if (response.status !== 200) {
const err = (await response.json());
throw new Error("D1_DUMP_ERROR", {
cause: new Error(err.error),
});
}
return await response.arrayBuffer();
}
async batch(statements) {
const exec = await this._send("/query", statements.map((s) => s.statement), statements.map((s) => s.params));
return exec;
}
async exec(query) {
const lines = query.trim().split("\n");
const exec = await this._send("/query", lines, []);
const error = exec
.map((r) => {
return r.error ? 1 : 0;
})
.indexOf(1);
if (error !== -1) {
throw new Error("D1_EXEC_ERROR", {
cause: new Error(`Error in line ${error + 1}: ${lines[error]}: ${exec[error].error}`),
});
}
else {
return {
count: exec.length,
duration: exec.reduce((p, c) => {
return p.duration + c.duration;
}),
};
}
}
async _send(endpoint, query, params) {
const body = JSON.stringify(typeof query == "object"
? query.map((s, index) => {
return { sql: s, params: params[index] };
})
: {
sql: query,
params: params,
});
const response = await this.binding.fetch(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
},
body,
});
if (response.status !== 200) {
const err = (await response.json());
throw new Error("D1_ERROR", { cause: new Error(err.error) });
}
const answer = await response.json();
return Array.isArray(answer) ? answer : answer;
}
}
class PreparedStatement {
statement;
database;
params;
constructor(database, statement, values) {
this.database = database;
this.statement = statement;
this.params = values || [];
}
bind(...values) {
return new PreparedStatement(this.database, this.statement, values);
}
async first(colName) {
const info = await this.database._send("/query", this.statement, this.params);
const results = info.results;
if (results.length < 1) {
throw new Error("D1_NORESULTS", { cause: new Error("No results") });
}
const result = results[0];
if (colName !== undefined) {
if (result[colName] === undefined) {
throw new Error("D1_COLUMN_NOTFOUND", {
cause: new Error(`Column not found`),
});
}
return result[colName];
}
else {
return result;
}
}
async run() {
return this.database._send("/execute", this.statement, this.params);
}
async all() {
return await this.database._send("/query", this.statement, this.params);
}
async raw() {
const s = await this.database._send("/query", this.statement, this.params);
const raw = [];
for (var r in s.results) {
const entry = Object.keys(s.results[r]).map((k) => {
return s.results[r][k];
});
raw.push(entry);
}
return raw;
}
}
+17 -120
View File
@@ -1,12 +1,9 @@
import config from '../../config.yaml'
import { Database } from '../d1'
import {
notifySlack,
notifyTelegram,
getCheckLocation,
getKVMonitors,
setKVMonitors,
notifyDiscord,
getMonitors,
} from './helpers'
function getDate() {
@@ -18,33 +15,21 @@ export async function processCronTrigger(event) {
const checkLocation = await getCheckLocation()
const checkDay = getDate()
// Get monitors state from KV
let monitorsState = await getKVMonitors()
const db = new Database(D1UNSAFE)
// Create empty state objects if not exists in KV storage yet
if (!monitorsState) {
monitorsState = { lastUpdate: {}, monitors: {} }
}
const monitors = await getMonitors()
let statements = []
const updateMonitorStatement = db.prepare('UPDATE monitors SET operational = ?2, last_updated = ?3 WHERE id = ?1')
const insertCheckStatement = db.prepare('INSERT INTO monitors_checks (monitor_id, location, res_ms, operational, date, timestamp) VALUES (?1, ?2, ?3, ?4, ?5, ?6)')
// Reset default all monitors state to true
monitorsState.lastUpdate.allOperational = true
for (const monitor of config.monitors) {
for (const monitor of monitors) {
// Create default monitor state if does not exist yet
if (typeof monitorsState.monitors[monitor.id] === 'undefined') {
monitorsState.monitors[monitor.id] = {
firstCheck: checkDay,
lastCheck: {},
checks: {},
}
}
console.log(`Checking ${monitor.name} ...`)
// Fetch the monitors URL
const init = {
method: monitor.method || 'GET',
redirect: monitor.followRedirect ? 'follow' : 'manual',
redirect: monitor.follow_redirect ? 'follow' : 'manual',
headers: {
'User-Agent': config.settings.user_agent || 'cf-worker-status-page',
},
@@ -57,103 +42,15 @@ export async function processCronTrigger(event) {
// Determine whether operational and status changed
const monitorOperational =
checkResponse.status === (monitor.expectStatus || 200)
const monitorStatusChanged =
monitorsState.monitors[monitor.id].lastCheck.operational !==
monitorOperational
// Save monitor's last check response status
monitorsState.monitors[monitor.id].lastCheck = {
status: checkResponse.status,
statusText: checkResponse.statusText,
operational: monitorOperational,
}
// Send Slack message on monitor change
if (
monitorStatusChanged &&
typeof SECRET_SLACK_WEBHOOK_URL !== 'undefined' &&
SECRET_SLACK_WEBHOOK_URL !== 'default-gh-action-secret'
) {
event.waitUntil(notifySlack(monitor, monitorOperational))
}
// Send Telegram message on monitor change
if (
monitorStatusChanged &&
typeof SECRET_TELEGRAM_API_TOKEN !== 'undefined' &&
SECRET_TELEGRAM_API_TOKEN !== 'default-gh-action-secret' &&
typeof SECRET_TELEGRAM_CHAT_ID !== 'undefined' &&
SECRET_TELEGRAM_CHAT_ID !== 'default-gh-action-secret'
) {
event.waitUntil(notifyTelegram(monitor, monitorOperational))
}
// Send Discord message on monitor change
if (
monitorStatusChanged &&
typeof SECRET_DISCORD_WEBHOOK_URL !== 'undefined' &&
SECRET_DISCORD_WEBHOOK_URL !== 'default-gh-action-secret'
) {
event.waitUntil(notifyDiscord(monitor, monitorOperational))
}
// make sure checkDay exists in checks in cases when needed
if (
(config.settings.collectResponseTimes || !monitorOperational) &&
!monitorsState.monitors[monitor.id].checks.hasOwnProperty(checkDay)
) {
monitorsState.monitors[monitor.id].checks[checkDay] = {
fails: 0,
res: {},
}
}
if (config.settings.collectResponseTimes && monitorOperational) {
// make sure location exists in current checkDay
if (
!monitorsState.monitors[monitor.id].checks[checkDay].res.hasOwnProperty(
checkLocation,
)
) {
monitorsState.monitors[monitor.id].checks[checkDay].res[
checkLocation
] = {
n: 0,
ms: 0,
a: 0,
}
}
// increment number of checks and sum of ms
const no = ++monitorsState.monitors[monitor.id].checks[checkDay].res[
checkLocation
].n
const ms = (monitorsState.monitors[monitor.id].checks[checkDay].res[
checkLocation
].ms += requestTime)
// save new average ms
monitorsState.monitors[monitor.id].checks[checkDay].res[
checkLocation
].a = Math.round(ms / no)
} else if (!monitorOperational) {
// Save allOperational to false
monitorsState.lastUpdate.allOperational = false
// Increment failed checks on status change or first fail of the day (maybe call it .incidents instead?)
if (monitorStatusChanged || monitorsState.monitors[monitor.id].checks[checkDay].fails == 0) {
monitorsState.monitors[monitor.id].checks[checkDay].fails++
}
}
checkResponse.status === (monitor.expect_status || 200) ? 1 : 0
statements.push(
insertCheckStatement.bind( monitor.id, checkLocation, requestTime, monitorOperational, checkDay, new Date().toISOString()),
updateMonitorStatement.bind( monitor.id, monitorOperational, new Date().toISOString())
)
}
// Save last update information
monitorsState.lastUpdate.time = Date.now()
monitorsState.lastUpdate.loc = checkLocation
const test = await db.batch(statements)
// Save monitorsState to KV storage
await setKVMonitors(monitorsState)
return new Response('OK')
return new Response(JSON.stringify(test))
}
+23
View File
@@ -1,5 +1,6 @@
import config from '../../config.yaml'
import { useEffect, useState } from 'react'
import { Database } from "../d1"; // this will be native at some point, see above
const kvDataKey = 'monitors_data_v1_1'
@@ -9,6 +10,28 @@ export async function getKVMonitors() {
//return JSON.parse(await KV_STATUS_PAGE.get(kvDataKey, 'text'))
}
export async function getMonitors() {
const db = new Database(D1UNSAFE)
const { results } = await db.prepare(
"SELECT * FROM monitors"
).all();
return results
}
export async function loadData() {
const db = new Database(D1UNSAFE)
const batch = await db.batch([
db.prepare(
"SELECT * FROM monitors"
),
db.prepare("SELECT monitor_id, AVG(res_ms) as avg_res, count(*) as count, location FROM monitors_checks WHERE date >= ?1 GROUP BY monitor_id, date, location ORDER BY timestamp ASC")
.bind(`${new Date().toISOString().split('T')[0]}`)
])
return { monitors: batch[0].results, checks: batch[1].results}
}
export async function setKVMonitors(data) {
return setKV(kvDataKey, JSON.stringify(data))
}
+12 -10
View File
@@ -1,19 +1,21 @@
name = "cf-workers-status-page"
workers_dev = true
account_id = ""
type = "webpack"
webpack_config = "node_modules/flareact/webpack"
compatibility_date = "2021-07-23"
compatibility_date = "2022-07-10"
main = "./worker/script.js"
routes = [
"status-d1.eidam.cf/*"
]
kv_namespaces = [{binding="KV_STATUS_PAGE", id="07c4d4e1aee94340abd97af34b2f78cc", preview_id="d5c948a04a3248898e05c84b684eeae2"}]
[triggers]
crons = ["* * * * *"]
[site]
bucket = "out"
entry-point = "./"
# uncomment and adjust following if you are not using GitHub Actions
#[env.production]
#kv-namespaces = [{binding="KV_STATUS_PAGE", id="xxxx", preview_id=""}]
#zone_id="xxx"
#route="xxx"
[[ unsafe.bindings ]]
name = "D1UNSAFE"
type = "d1"
id = "9fefade8-08c7-41fe-83a5-81417eb955df"
+2968 -2007
View File
File diff suppressed because it is too large Load Diff