Compare commits

..

25 Commits

Author SHA1 Message Date
Adam Janiš 3898ffcbc7 d1 init 2022-07-11 09:48:07 +01:00
corvofeng 7813ff93ac Replace all '-' for monitor name.
Signed-off-by: corvofeng <corvofeng@gmail.com>
2022-02-15 22:08:15 +00:00
Chris Buckley 596d30389f Increment failed checks at least once
Currently, a failure is only recorded if a monitor transitions from operational
to not in a given day. If the monitor is non-operational at the start of the day,
or remains non-operational for a full day, the failure will not be recorded.
2021-11-18 11:22:15 +00:00
Chris Buckley 1189e708da Supply fallback text for Slack message
This is used as a plain-text summary of the attachment, e.g. in push notifications.

Docs: https://api.slack.com/reference/messaging/attachments#legacy_fields
2021-10-11 14:21:30 +01:00
rexhaugen de0cfaf504 Update wrangler.toml
Add "compatibility_date" for Cloudflare to prevent jq json parse issues.
2021-10-11 14:20:37 +01:00
Chris Buckley f0b27a8446 Add more data centre locations
The full list of locations was generated from the [Cloudflare Status Page](https://www.cloudflarestatus.com/):

```bash
{
  echo 'export const locations = {';
  curl -ks https://www.cloudflarestatus.com/ | grep -- '- (' | while read line; do
    code=$(awk -F '[()]' '{print $2}' <<< "$line");
    city=$(awk -F '[,-]' '{print $1}' <<< "$line");
    echo "  $code: '${city//\'/’}',";
  done | sort;
  echo '}';
} > src/functions/locations.js
```
2021-10-11 14:13:49 +01:00
dependabot[bot] a8cfe25c9a chore(deps): bump y18n from 4.0.0 to 4.0.3
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.3.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/y18n-v4.0.3/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/compare/v4.0.0...y18n-v4.0.3)

---
updated-dependencies:
- dependency-name: y18n
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-23 09:34:13 +02:00
dependabot[bot] df64cdc113 chore(deps): bump color-string from 1.5.4 to 1.6.0
Bumps [color-string](https://github.com/Qix-/color-string) from 1.5.4 to 1.6.0.
- [Release notes](https://github.com/Qix-/color-string/releases)
- [Changelog](https://github.com/Qix-/color-string/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Qix-/color-string/compare/1.5.4...1.6.0)

---
updated-dependencies:
- dependency-name: color-string
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-23 09:34:07 +02:00
Marcello Bachechi d87223f292 Added documentation 2021-07-23 09:19:34 +02:00
Marcello Bachechi a5a65a7582 Updated config to reflect linkable option 2021-07-23 09:19:34 +02:00
Marcello Bachechi 23673587c2 Update monitorCard.js 2021-07-23 09:19:34 +02:00
Marcello Bachechi fa9d865cc1 Added documentation on how to get running locally 2021-07-23 09:17:54 +02:00
dependabot[bot] 0fac88d7f8 chore(deps): bump ws from 6.2.1 to 6.2.2
Bumps [ws](https://github.com/websockets/ws) from 6.2.1 to 6.2.2.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/6.2.1...6.2.2)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-08 22:48:47 +02:00
dependabot[bot] 7a9a2f90ef chore(deps-dev): bump postcss from 8.1.8 to 8.2.10
Bumps [postcss](https://github.com/postcss/postcss) from 8.1.8 to 8.2.10.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.1.8...8.2.10)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-08 22:48:39 +02:00
dependabot[bot] c337b55c37 chore(deps): bump hosted-git-info from 2.8.8 to 2.8.9
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

---
updated-dependencies:
- dependency-name: hosted-git-info
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-08 22:48:22 +02:00
dependabot[bot] f0a4974e92 chore(deps): bump elliptic from 6.5.3 to 6.5.4
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

---
updated-dependencies:
- dependency-name: elliptic
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-08 22:48:17 +02:00
dependabot[bot] cb15831994 chore(deps): bump browserslist from 4.14.7 to 4.16.6
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.14.7 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.14.7...4.16.6)

---
updated-dependencies:
- dependency-name: browserslist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-08 22:48:04 +02:00
dependabot[bot] 2868f9820c chore(deps): bump lodash from 4.17.20 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

---
updated-dependencies:
- dependency-name: lodash
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-08 22:47:49 +02:00
dependabot[bot] 48d78d117a chore(deps): bump url-parse from 1.4.7 to 1.5.1
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.4.7 to 1.5.1.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.4.7...1.5.1)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-08 22:47:25 +02:00
dependabot[bot] c7f642cd8c chore(deps): bump dns-packet from 1.3.1 to 1.3.4
Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 1.3.1 to 1.3.4.
- [Release notes](https://github.com/mafintosh/dns-packet/releases)
- [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mafintosh/dns-packet/compare/v1.3.1...v1.3.4)

---
updated-dependencies:
- dependency-name: dns-packet
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-08 22:47:16 +02:00
dependabot[bot] 54bdd42fc1 chore(deps): bump ssri from 6.0.1 to 6.0.2
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

---
updated-dependencies:
- dependency-name: ssri
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-08 22:47:05 +02:00
Adam Janiš bbf9def61f Update README.md 2021-03-07 23:43:12 +01:00
Anthony Graignic ccec34ff5d Add Discord notification (#35)
add discord notification
2021-02-23 13:32:08 +01:00
endriu3314 ee586c9e64 Add focus ring and remove default focus outline 2021-01-29 18:33:26 +01:00
Adam Janiš 8d70a0f992 Create FUNDING.yml 2021-01-23 20:20:18 +01:00
21 changed files with 3695 additions and 2264 deletions
+4
View File
@@ -0,0 +1,4 @@
# These are supported funding model platforms
github: eidam
ko_fi: eidam
+3
View File
@@ -34,15 +34,18 @@ jobs:
[ -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
[ -z "$SECRET_TELEGRAM_API_TOKEN" ] && echo "Secret SECRET_TELEGRAM_API_TOKEN not set, creating dummy one..." && SECRET_TELEGRAM_API_TOKEN="default-gh-action-secret" || true
[ -z "$SECRET_TELEGRAM_CHAT_ID" ] && echo "Secret SECRET_TELEGRAM_CHAT_ID not set, creating dummy one..." && SECRET_TELEGRAM_CHAT_ID="default-gh-action-secret" || true
[ -z "$SECRET_DISCORD_WEBHOOK_URL" ] && echo "Secret SECRET_DISCORD_WEBHOOK_URL not set, creating dummy one..." && SECRET_DISCORD_WEBHOOK_URL="default-gh-action-secret" || true
postCommands: |
yarn kv-gc
secrets: |
SECRET_SLACK_WEBHOOK_URL
SECRET_TELEGRAM_API_TOKEN
SECRET_TELEGRAM_CHAT_ID
SECRET_DISCORD_WEBHOOK_URL
environment: production
env:
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
SECRET_SLACK_WEBHOOK_URL: ${{secrets.SECRET_SLACK_WEBHOOK_URL}}
SECRET_TELEGRAM_API_TOKEN: ${{secrets.SECRET_TELEGRAM_API_TOKEN}}
SECRET_TELEGRAM_CHAT_ID: ${{secrets.SECRET_TELEGRAM_CHAT_ID}}
SECRET_DISCORD_WEBHOOK_URL: ${{secrets.SECRET_DISCORD_WEBHOOK_URL}}
+54 -4
View File
@@ -19,6 +19,7 @@ Also, prepare the following secrets
- Cloudflare API token with `Edit Cloudflare Workers` permissions
- Slack incoming webhook \(optional\)
- Discord incoming webhook \(optional\)
## Getting started
@@ -38,6 +39,9 @@ You can either deploy with **Cloudflare Deploy Button** using GitHub Actions or
- Name: SECRET_SLACK_WEBHOOK_URL (optional)
- Value: your-slack-webhook-url
- Name: SECRET_DISCORD_WEBHOOK_URL (optional)
- Value: your-discord-webhook-url
```
3. Navigate to the **Actions** settings in your repository and enable them
@@ -46,7 +50,7 @@ You can either deploy with **Cloudflare Deploy Button** using GitHub Actions or
```yaml
settings:
title: 'Status Page'
url: 'https://status-page.eidam.dev' # used for Slack messages
url: 'https://status-page.eidam.dev' # used for Slack & Discord messages
logo: logo-192x192.png # image in ./public/ folder
daysInHistogram: 90 # number of days you want to display in histogram
collectResponseTimes: false # experimental feature, enable only for <5 monitors or on paid plans
@@ -70,6 +74,7 @@ You can either deploy with **Cloudflare Deploy Button** using GitHub Actions or
method: GET # default=GET
expectStatus: 200 # operational status, default=200
followRedirect: false # should fetch follow redirects, default=false
linkable: false # should the titles be links to the service, default=true
```
5. Push to `main` branch to trigger the deployment
@@ -96,6 +101,7 @@ You can clone the repository yourself and use Wrangler CLI to develop/deploy, ex
- create KV namespace and add the `KV_STATUS_PAGE` binding to [wrangler.toml](./wrangler.toml)
- create Worker secrets _\(optional\)_
- `SECRET_SLACK_WEBHOOK_URL`
- `SECRET_DISCORD_WEBHOOK_URL`
## Workers KV free tier
@@ -115,6 +121,50 @@ The Workers Free plan includes limited KV usage, but the quota is sufficient for
## Future plans
Stay tuned for more features coming in, like leveraging the fact that CRON instances are scheduled around the world during the day
so we can monitor the response times. However, we will most probably wait for the [Durable Objects](https://blog.cloudflare.com/introducing-workers-durable-objects/) to be in open beta
as they are better fit to reliably store such info.
WIP - Support for Durable Objects - Cloudflare's product for low-latency coordination and consistent storage for the Workers platform. There is a working prototype, however, we are waiting for at least open beta.
There is also a managed version of this project, currently in beta. Feel free to check it out https://statusflare.com (https://twitter.com/statusflare_com).
## Running project locally
**Requirements**
- Linux or WSL
- Yarn (`npm i -g yarn`)
- Node 14+
### Steps to get server up and running
**Install wrangler**
```
npm i -g wrangler
```
**Login With Wrangler to Cloudflare**
```
wrangler login
```
**Create your KV namespace in cloudflare**
```
On the workers page navigate to KV, and create a namespace
```
**Update your wrangler.toml with**
```
kv-namespaces = [{binding="KV_STATUS_PAGE", id="<KV_ID>", preview_id="<KV_ID>"}]
```
_Note: you may need to change `kv-namespaces` to `kv_namespaces`_
**Install packages**
```
yarn install
```
**Create CSS**
```
yarn run css
```
**Run**
```
yarn run dev
```
_Note: If the styles do not come through try using `localhost:8787` instead of `localhost:8080`_
+1 -23
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,25 +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
- 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
- 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.1.8",
"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)
}
+10 -27
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,16 +59,15 @@ 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 )}
/>
)
})}
+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
);
+22 -8
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,18 +33,29 @@ export default function MonitorCard({ key, monitor, data }) {
</div>
</div>
)}
<a href={monitor.url} target="_blank">
<div className="text-xl">{monitor.name}</div>
</a>
{monitor.linkable ?
(
<a href={monitor.url} target="_blank">
<div className="text-xl">{monitor.name}</div>
</a>
)
:
(
<span>
<div className="text-xl">{monitor.name}</div>
</span>
)
}
</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>
)
+1 -1
View File
@@ -1,4 +1,4 @@
import { locations } from '../functions/helpers'
import { locations } from '../functions/locations'
export default function MonitorDayAverage({ location, avg }) {
return (
+12 -7
View File
@@ -1,5 +1,5 @@
import config from '../../config.yaml'
import { locations } from '../functions/helpers'
import { locations } from '../functions/locations'
const classes = {
green:
@@ -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 {
+2 -2
View File
@@ -45,11 +45,11 @@ export default function ThemeSwitcher() {
setDark(!darkmode)
}
const buttonColor = darkmode ? 'bg-gray-700' : 'bg-gray-200'
const buttonColor = darkmode ? 'bg-gray-700 focus:ring-gray-700' : 'bg-gray-200 focus:ring-gray-200'
return (
<button
className={`${buttonColor} rounded-full h-7 w-7 mr-4`}
className={`${buttonColor} rounded-full h-7 w-7 mr-4 focus:outline-none focus:ring-2 focus:ring-opacity-50`}
onClick={changeTheme}
>
{darkmode ? sunIcon : moonIcon}
+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 -110
View File
@@ -1,11 +1,9 @@
import config from '../../config.yaml'
import { Database } from '../d1'
import {
notifySlack,
notifyTelegram,
getCheckLocation,
getKVMonitors,
setKVMonitors,
getMonitors,
} from './helpers'
function getDate() {
@@ -17,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',
},
@@ -56,94 +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))
}
// 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, only on status change (maybe call it .incidents instead?)
if (monitorStatusChanged) {
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))
}
+49 -8
View File
@@ -1,21 +1,37 @@
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'
export const locations = {
WAW: 'Warsaw',
SCL: 'Santiago de Chile',
MEL: 'Melbourne',
SIN: 'Singapore',
}
export async function getKVMonitors() {
// trying both to see performance difference
return KV_STATUS_PAGE.get(kvDataKey, 'json')
//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))
}
@@ -34,6 +50,7 @@ export async function notifySlack(monitor, operational) {
const payload = {
attachments: [
{
fallback: `Monitor ${monitor.name} changed status to ${getOperationalLabel(operational)}`,
color: operational ? '#36a64f' : '#f2c744',
blocks: [
{
@@ -70,7 +87,7 @@ export async function notifySlack(monitor, operational) {
}
export async function notifyTelegram(monitor, operational) {
const text = `Monitor *${monitor.name.replace(
const text = `Monitor *${monitor.name.replaceAll(
'-',
'\\-',
)}* changed status to *${getOperationalLabel(operational)}*
@@ -90,6 +107,30 @@ export async function notifyTelegram(monitor, operational) {
})
}
// Visualize your payload using https://leovoel.github.io/embed-visualizer/
export async function notifyDiscord(monitor, operational) {
const payload = {
username: `${config.settings.title}`,
avatar_url: `${config.settings.url}/${config.settings.logo}`,
embeds: [
{
title: `${monitor.name} is ${getOperationalLabel(operational)} ${
operational ? ':white_check_mark:' : ':x:'
}`,
description: `\`${monitor.method ? monitor.method : 'GET'} ${
monitor.url
}\` - :eyes: [Status Page](${config.settings.url})`,
color: operational ? 3581519 : 13632027,
},
],
}
return fetch(SECRET_DISCORD_WEBHOOK_URL, {
body: JSON.stringify(payload),
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
}
export function useKeyPress(targetKey) {
const [keyPressed, setKeyPressed] = useState(false)
+226
View File
@@ -0,0 +1,226 @@
export const locations = {
ADL: 'Adelaide',
AKL: 'Auckland',
ALG: 'Algiers',
AMM: 'Amman',
AMS: 'Amsterdam',
ARI: 'Arica',
ARN: 'Stockholm',
ASU: 'Asunción',
ATH: 'Athens',
ATL: 'Atlanta',
BAH: 'Manama',
BCN: 'Barcelona',
BEG: 'Belgrade',
BEL: 'Belém',
BEY: 'Beirut',
BGW: 'Baghdad',
BKK: 'Bangkok',
BLR: 'Bangalore',
BNA: 'Nashville',
BNE: 'Brisbane',
BNU: 'Blumenau',
BOG: 'Bogotá',
BOM: 'Mumbai',
BOS: 'Boston',
BRU: 'Brussels',
BSB: 'Brasilia',
BUD: 'Budapest',
BUF: 'Buffalo',
BWN: 'Bandar Seri Begawan',
CAN: 'Guangzhou',
CBR: 'Canberra',
CCU: 'Kolkata',
CDG: 'Paris',
CEB: 'Cebu',
CFC: 'Caçador',
CGK: 'Jakarta',
CGO: 'Zhengzhou',
CGP: 'Chittagong',
CKG: 'Chongqing',
CLT: 'Charlotte',
CMB: 'Colombo',
CMH: 'Columbus',
CMN: 'Casablanca',
CNF: 'Belo Horizonte',
CPH: 'Copenhagen',
CPT: 'Cape Town',
CSX: 'Zhuzhou',
CTU: 'Chengdu',
CUR: 'Willemstad',
CWB: 'Curitiba',
DAC: 'Dhaka',
DAR: 'Dar Es Salaam',
DEL: 'New Delhi',
DEN: 'Denver',
DFW: 'Dallas',
DKR: 'Dakar',
DME: 'Moscow',
DMM: 'Dammam',
DOH: 'Doha',
DTW: 'Detroit',
DUB: 'Dublin',
DUR: 'Durban',
DUS: 'Düsseldorf',
DXB: 'Dubai',
EDI: 'Edinburgh',
EVN: 'Yerevan',
EWR: 'Newark',
EZE: 'Buenos Aires',
FCO: 'Rome',
FLN: 'Florianopolis',
FOR: 'Fortaleza',
FRA: 'Frankfurt',
GIG: 'Rio de Janeiro',
GND: 'St. Georges',
GOT: 'Gothenburg',
GRU: 'São Paulo',
GUA: 'Guatemala City',
GVA: 'Geneva',
GYD: 'Baku',
GYE: 'Guayaquil',
HAM: 'Hamburg',
HAN: 'Hanoi',
HEL: 'Helsinki',
HKG: 'Hong Kong ',
HNL: 'Honolulu',
HRE: 'Harare',
HYD: 'Hyderabad',
IAD: 'Ashburn',
IAH: 'Houston',
ICN: 'Seoul',
IND: 'Indianapolis',
ISB: 'Islamabad',
IST: 'Istanbul',
ITJ: 'Itajaí',
JAX: 'Jacksonville',
JIB: 'Djibouti City',
JNB: 'Johannesburg',
JSR: 'Jashore',
KBP: 'Kyiv',
KEF: 'Reykjavík',
KGL: 'Kigali',
KHI: 'Karachi',
KIV: 'Chișinău',
KIX: 'Osaka',
KJA: 'Krasnoyarsk',
KTM: 'Kathmandu',
KUL: 'Kuala Lumpur',
KWI: 'Kuwait City',
LAD: 'Luanda',
LAS: 'Las Vegas',
LAX: 'Los Angeles',
LCA: 'Nicosia',
LED: 'Saint Petersburg',
LHE: 'Lahore',
LHR: 'London',
LIM: 'Lima',
LIS: 'Lisbon',
LOS: 'Lagos',
LUX: 'Luxembourg City',
MAA: 'Chennai',
MAD: 'Madrid',
MAN: 'Manchester',
MBA: 'Mombasa',
MCI: 'Kansas City',
MCT: 'Muscat',
MDE: 'Medellín',
MEL: 'Melbourne',
MEM: 'Memphis',
MEX: 'Mexico City',
MFE: 'McAllen',
MFM: 'Macau ',
MGM: 'Montgomery',
MIA: 'Miami',
MLE: 'Malé',
MNL: 'Manila',
MPM: 'Maputo',
MRS: 'Marseille',
MRU: 'Port Louis',
MSP: 'Minneapolis',
MUC: 'Munich',
MXP: 'Milan',
NAG: 'Nagpur',
NBG: 'Ningbo',
NBO: 'Nairobi',
NOU: 'Noumea',
NRT: 'Tokyo',
OMA: 'Omaha',
ORD: 'Chicago',
ORF: 'Norfolk',
OSL: 'Oslo',
OTP: 'Bucharest',
PAP: 'Port',
PBH: 'Thimphu',
PBM: 'Paramaribo',
PDX: 'Portland',
PER: 'Perth',
PHL: 'Philadelphia',
PHX: 'Phoenix',
PIT: 'Pittsburgh',
PMO: 'Palermo',
PNH: 'Phnom Penh',
POA: 'Porto Alegre',
PRG: 'Prague',
PTY: 'Panama City',
QRO: 'Queretaro',
QWJ: 'Americana',
RAO: 'Ribeirao Preto',
RGN: 'Yangon',
RIC: 'Richmond',
RIX: 'Riga',
ROB: 'Monrovia',
RUH: 'Riyadh',
RUN: 'Réunion',
SAN: 'San Diego',
SCL: 'Santiago',
SEA: 'Seattle',
SGN: 'Ho Chi Minh City',
SHA: 'Shanghai',
SIN: 'Singapore',
SJC: 'San Jose',
SJO: 'San José',
SJP: 'São José do Rio Preto',
SKG: 'Thessaloniki',
SLC: 'Salt Lake City',
SMF: 'Sacramento',
SOD: 'Sorocaba',
SOF: 'Sofia',
SSA: 'Salvador',
STL: 'St. Louis',
SVX: 'Yekaterinburg',
SYD: 'Sydney',
SZV: 'Suzhou',
TBS: 'Tbilisi',
TGU: 'Tegucigalpa',
TLH: 'Tallahassee',
TLL: 'Tallinn',
TLV: 'Tel Aviv',
TNA: 'Jinan',
TNR: 'Antananarivo',
TPA: 'Tampa',
TPE: 'Taipei ',
TSN: 'Tianjin',
TUN: 'Tunis',
TXL: 'Berlin',
UIO: 'Quito',
ULN: 'Ulaanbaatar',
URT: 'Surat Thani',
VCP: 'Campinas',
VIE: 'Vienna',
VNO: 'Vilnius',
VTE: 'Vientiane',
WAW: 'Warsaw',
WUH: 'Wuhan',
WUX: 'Wuxi',
XIY: 'Xian',
YUL: 'Montréal',
YVR: 'Vancouver',
YWG: 'Winnipeg',
YXE: 'Saskatoon',
YYC: 'Calgary',
YYZ: 'Toronto',
ZAG: 'Zagreb',
ZDM: 'Ramallah ',
ZRH: 'Zürich',
}
+12 -9
View File
@@ -1,18 +1,21 @@
name = "cf-workers-status-page"
workers_dev = true
account_id = ""
type = "webpack"
webpack_config = "node_modules/flareact/webpack"
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"
+3007 -2055
View File
File diff suppressed because it is too large Load Diff