From e85c5766a78fc9be534dc80df64782cd4fb159d9 Mon Sep 17 00:00:00 2001 From: Adam Janis Date: Sun, 8 Nov 2020 13:56:02 +0100 Subject: [PATCH] init --- .cargo-ok | 0 .github/workflows/deploy.yml | 21 ++ .gitignore | 29 ++ .prettierrc | 7 + config.yaml | 378 +++++++++++++++++++++++++++ flareact.config.js | 11 + index.js | 32 +++ out/.gitkeep | 0 package.json | 23 ++ pages/api/triggerCron.js | 5 + pages/index.js | 141 ++++++++++ public/favicon.ico | Bin 0 -> 15406 bytes public/logo-192x192.png | Bin 0 -> 10205 bytes public/main.css | 66 +++++ src/components/monitorHistogram.js | 49 ++++ src/components/monitorStatusLabel.js | 16 ++ src/css/index.css | 66 +++++ src/functions/cronTrigger.js | 57 ++++ src/functions/helpers.js | 89 +++++++ wrangler.toml | 23 ++ 20 files changed, 1013 insertions(+) create mode 100644 .cargo-ok create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 config.yaml create mode 100644 flareact.config.js create mode 100644 index.js create mode 100644 out/.gitkeep create mode 100644 package.json create mode 100644 pages/api/triggerCron.js create mode 100644 pages/index.js create mode 100644 public/favicon.ico create mode 100644 public/logo-192x192.png create mode 100644 public/main.css create mode 100644 src/components/monitorHistogram.js create mode 100644 src/components/monitorStatusLabel.js create mode 100644 src/css/index.css create mode 100644 src/functions/cronTrigger.js create mode 100644 src/functions/helpers.js create mode 100644 wrangler.toml diff --git a/.cargo-ok b/.cargo-ok new file mode 100644 index 000000000..e69de29bb diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..74b4951bc --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,21 @@ +name: Deploy +on: + - repository_dispatch +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 12 + - run: yarn install + - run: yarn build + - name: Publish + uses: cloudflare/wrangler-action@1.2.0 + with: + apiToken: ${{ secrets.CF_API_TOKEN }} + env: + CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} + IS_WORKER: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..57abef40d --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +out/* +!out/.gitkeep + +# production +/dist/ + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env + +/worker diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..a06a385e3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "semi": false, + "trailingComma": "all", + "tabWidth": 2, + "printWidth": 80 +} diff --git a/config.yaml b/config.yaml new file mode 100644 index 000000000..def54e6d3 --- /dev/null +++ b/config.yaml @@ -0,0 +1,378 @@ +settings: + title: "Status Page" + logo: logo-192x192.png + daysInHistory: 90 + + allmonitorsOperational: "All Systems Operational" + notAllmonitorsOperational: "Not All Systems Operational" + monitorLabelOperational: "Operational" + monitorLabelNotOperational: "Not great not terrible" + monitorLabelNoData: "No data" + + +monitors: + - id: kiwi-com-homepage + name: Kiwi.com homepage + description: Kiwi.com en homepage + url: 'https://www.kiwi.com/en/' + method: GET + expectStatus: 200 + followRedirect: false + + - id: eidam-dev + name: Cheesy Status Page + description: 'status-page.eidam.dev' + url: 'https://status-page.eidam.dev/' + method: GET + expectStatus: 200 + + - id: google-com + name: Google.com + description: Google homepage + url: 'https://www.google.com' + method: GET + expectStatus: 200 + + - id: cf-workers-status-page + name: This Workers Status Page project made public + description: /shrug + url: 'https://github.com/adam-janis/cf-workers-status-page' + method: GET + expectStatus: 200 + + - id: kiwicomapi-cn + name: Some other site + description: Is this done yet? + url: 'http://kiwicomapi.cn/' + method: GET + expectStatus: 200 + + - id: testy-testy + name: Testy testy + description: Something /shrug + url: 'http://kiwicomapiiii.cn/' + method: GET + expectStatus: 200 + + - id: hello-world + name: Hello World + url: 'http://cnn.cn/' + method: GET + expectStatus: 200 + + + + + + - id: eidam-dev-2 + name: Eidam.dev + description: 'Eidam.dev homepage, there is none' + url: 'https://eidam.dev' + method: GET + expectStatus: 403 + + - id: google-com-2 + name: Google.com + description: Google homepage + url: 'https://www.google.com' + method: GET + expectStatus: 200 + + - id: cf-workers-status-page-2 + name: This Workers Status Page project made public + description: /shrug + url: 'https://github.com/adam-janis/cf-workers-status-page' + method: GET + expectStatus: 200 + + - id: kiwicomapi-cn-2 + name: Kiwi.com API CN + description: Is this done yet? + url: 'http://kiwicomapi.cn/' + method: GET + expectStatus: 200 + + - id: testy-testy-2 + name: Testy testy + description: Something /shrug + url: 'http://kiwicomapiiii.cn/' + method: GET + expectStatus: 200 + + - id: hello-world-2 + name: Hello World + url: 'http://cnn.cn/' + method: GET + expectStatus: 200 + + + + - id: eidam-dev-22 + name: Eidam.dev + description: 'Eidam.dev homepage, there is none' + url: 'https://eidam.dev' + method: GET + expectStatus: 403 + + - id: google-com-22 + name: Google.com + description: Google homepage + url: 'https://www.google.com' + method: GET + expectStatus: 200 + + - id: cf-workers-status-page-22 + name: This Workers Status Page project made public + description: /shrug + url: 'https://github.com/adam-janis/cf-workers-status-page' + method: GET + expectStatus: 200 + + - id: kiwicomapi-cn-22 + name: Kiwi.com API CN + description: Is this done yet? + url: 'http://kiwicomapi.cn/' + method: GET + expectStatus: 200 + + - id: testy-testy-22 + name: Testy testy + description: Something /shrug + url: 'http://kiwicomapiiii.cn/' + method: GET + expectStatus: 200 + + - id: hello-world-22 + name: Hello World + url: 'http://cnn.cn/' + method: GET + expectStatus: 200 + + + - id: eidam-dev-333 + name: Eidam.dev + description: 'Eidam.dev homepage, there is none' + url: 'https://eidam.dev' + method: GET + expectStatus: 403 + + - id: google-com-333 + name: Google.com + description: Google homepage + url: 'https://www.google.com' + method: GET + expectStatus: 200 + + - id: cf-workers-status-page-333 + name: This Workers Status Page project made public + description: /shrug + url: 'https://github.com/adam-janis/cf-workers-status-page' + method: GET + expectStatus: 200 + + - id: kiwicomapi-cn-333 + name: Kiwi.com API CN + description: Is this done yet? + url: 'http://kiwicomapi.cn/' + method: GET + expectStatus: 200 + + - id: testy-testy-333 + name: Testy testy + description: Something /shrug + url: 'http://kiwicomapiiii.cn/' + method: GET + expectStatus: 200 + + - id: hello-world-333 + name: Hello World + url: 'http://cnn.cn/' + method: GET + expectStatus: 200 + + + + + + + + + + + - id: 25-eidam-dev + name: Eidam.dev + description: 'Eidam.dev homepage, there is none' + url: 'https://eidam.dev' + method: GET + expectStatus: 403 + + - id: 25-google-com + name: Bing.com + description: Bing homepage + url: 'https://www.google.com' + method: GET + expectStatus: 200 + + - id: 25-cf-workers-status-page + name: This Workers Status Page project made public + description: /shrug + url: 'https://github.com/adam-janis/cf-workers-status-page' + method: GET + expectStatus: 200 + + - id: 25-kiwicomapi-cn + name: Kiwi.com API CN + description: Is this done yet? + url: 'http://kiwicomapi.cn/' + method: GET + expectStatus: 200 + + - id: 25-testy-testy + name: Testy testy + description: Something /shrug + url: 'http://kiwicomapiiii.cn/' + method: GET + expectStatus: 200 + + - id: 25-hello-world + name: Hello World + url: 'http://cnn.cn/' + method: GET + expectStatus: 200 + + + + + + - id: 25-eidam-dev-2 + name: Seznam.cz + description: 'Just seznam' + url: 'https://eidam.dev' + method: GET + expectStatus: 403 + + - id: 25-google-com-2 + name: Google.com + description: Google homepage + url: 'https://www.google.com' + method: GET + expectStatus: 200 + + - id: 25-cf-workers-status-page-2 + name: This Workers Status Page project made public + description: /shrug + url: 'https://github.com/adam-janis/cf-workers-status-page' + method: GET + expectStatus: 200 + + - id: 25-kiwicomapi-cn-2 + name: Kiwi.com API CN + description: Is this done yet? + url: 'http://kiwicomapi.cn/' + method: GET + expectStatus: 200 + + - id: 25-testy-testy-2 + name: Testy testy + description: Something /shrug + url: 'http://kiwicomapiiii.cn/' + method: GET + expectStatus: 200 + + - id: 25-hello-world-2 + name: Hello World + url: 'http://cnn.cn/' + method: GET + expectStatus: 200 + + + + - id: 25-eidam-dev-22 + name: Eidam.dev + description: 'Eidam.dev homepage, there is none' + url: 'https://eidam.dev' + method: GET + expectStatus: 403 + + - id: 25-google-com-22 + name: Google.com + description: Google homepage + url: 'https://www.google.com' + method: GET + expectStatus: 200 + + - id: 25-cf-workers-status-page-22 + name: This Workers Status Page project made public + description: /shrug + url: 'https://github.com/adam-janis/cf-workers-status-page' + method: GET + expectStatus: 200 + + - id: 25-kiwicomapi-cn-22 + name: Something totally different + description: Is this done yet? + url: 'http://kiwicomapi.cn/' + method: GET + expectStatus: 200 + + - id: 25-testy-testy-22 + name: Testy testy + description: Something /shrug + url: 'http://kiwicomapiiii.cn/' + method: GET + expectStatus: 200 + + - id: 25-hello-world-22 + name: Hello World + url: 'http://cnn.cn/' + method: GET + expectStatus: 200 + + + - id: 25-eidam-dev-333 + name: Eidam.dev + description: 'Eidam.dev homepage, there is none' + url: 'https://eidam.dev' + method: GET + expectStatus: 403 + + - id: 25-google-com-333 + name: Google.com + description: Google homepage + url: 'https://www.google.com' + method: GET + expectStatus: 200 + + - id: 25-cf-workers-status-page-333 + name: This Workers Status Page project made public + description: /shrug + url: 'https://github.com/adam-janis/cf-workers-status-page' + method: GET + expectStatus: 200 + + - id: 25-kiwicomapi-cn-333 + name: Kiwi.com API CN + description: Is this done yet? + url: 'http://kiwicomapi.cn/' + method: GET + expectStatus: 200 + + - id: 25-testy-testy-333 + name: Testy testy + description: Something /shrug + url: 'http://kiwicomapiiii.cn/' + method: GET + expectStatus: 200 + + - id: 25-hello-world-333 + name: Hello World + url: 'http://cnn.cn/' + method: GET + expectStatus: 200 + + - id: 25-hello-world-333 + name: Hello World + url: 'http://cnn.cn/' + method: GET + expectStatus: 200 + diff --git a/flareact.config.js b/flareact.config.js new file mode 100644 index 000000000..8bd3f87dc --- /dev/null +++ b/flareact.config.js @@ -0,0 +1,11 @@ +module.exports = { + webpack: (config, options) => { + config.module.rules.push({ + test: /\.ya?ml$/, + type: 'json', + use: 'yaml-loader', + }) + + return config + }, +} diff --git a/index.js b/index.js new file mode 100644 index 000000000..fd9daf4b8 --- /dev/null +++ b/index.js @@ -0,0 +1,32 @@ +import { handleEvent } from 'flareact' +import { processCronTrigger } from './src/functions/cronTrigger' + +/** + * The DEBUG flag will do two things that help during development: + * 1. we will skip caching on the edge, which makes it easier to + * debug. + * 2. we will return an error message on exception in your Response rather + * than the default 404.html page. + */ +const DEBUG = false + +addEventListener('fetch', event => { + try { + event.respondWith( + handleEvent(event, require.context('./pages/', true, /\.js$/), DEBUG), + ) + } catch (e) { + if (DEBUG) { + return event.respondWith( + new Response(e.message || e.toString(), { + status: 500, + }), + ) + } + event.respondWith(new Response('Internal Error', { status: 500 })) + } +}) + +addEventListener('scheduled', event => { + event.waitUntil(processCronTrigger(event)) +}) diff --git a/out/.gitkeep b/out/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/package.json b/package.json new file mode 100644 index 000000000..d1970a29f --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "cf-workers-status-page", + "version": "1.0.0", + "author": "Adam Janiš ", + "license": "MIT", + "main": "index.js", + "private": true, + "scripts": { + "dev": "flareact dev", + "build": "flareact build", + "deploy": "flareact publish", + "format": "prettier --write '**/*.{js,css,json,md}'" + }, + "dependencies": { + "flareact": "^0.7.1", + "react": "^16.13.1", + "react-dom": "^16.13.1" + }, + "devDependencies": { + "prettier": "^1.18.2", + "yaml-loader": "^0.6.0" + } +} diff --git a/pages/api/triggerCron.js b/pages/api/triggerCron.js new file mode 100644 index 000000000..a64d33fa8 --- /dev/null +++ b/pages/api/triggerCron.js @@ -0,0 +1,5 @@ +import { processCronTrigger } from '../../src/functions/cronTrigger' + +export default async event => { + return processCronTrigger() +} diff --git a/pages/index.js b/pages/index.js new file mode 100644 index 000000000..995279f9c --- /dev/null +++ b/pages/index.js @@ -0,0 +1,141 @@ +import Head from 'flareact/head' +import MonitorHistogram from '../src/components/monitorHistogram' + +import { + getLastUpdate, + getMonitors, + getMonitorsHistory, +} from '../src/functions/helpers' + +import config from '../config.yaml' +import MonitorStatusLabel from '../src/components/monitorStatusLabel' + +export async function getEdgeProps() { + // get KV data + const kvMonitors = await getMonitors() + const kvMonitorsDays = await getMonitorsHistory() + const kvLastUpdate = await getLastUpdate() + + // prepare data maps for components + let monitorsOperational = true + let kvMonitorsMap = {} + kvMonitors.forEach(x => { + kvMonitorsMap[x.metadata.id] = x.metadata + if (x.metadata.operational === false) monitorsOperational = false + }) + + let kvMonitorsDaysMap = {} + kvMonitorsDays.forEach(x => { + kvMonitorsDaysMap[x.name] = x.metadata.operational + }) + + return { + props: { + config, + kvMonitorsMap, + kvMonitorsDaysMap, + monitorsOperational, + kvLastUpdate, + }, + // Revalidate these props once every x seconds + revalidate: 5, + } +} + +export default function Index({ + config, + kvMonitorsMap, + kvMonitorsDaysMap, + monitorsOperational, + kvLastUpdate, +}) { + return ( +
+ + {config.settings.title} + + + +
+

+ + {config.settings.title} +

+
+
+
+ {monitorsOperational + ? config.settings.allmonitorsOperational + : config.settings.notAllmonitorsOperational} +
+
+ checked {Math.round((Date.now() - kvLastUpdate) / 1000)} sec ago +
+
+
+ {config.monitors.map((monitor, key) => { + return ( +
+
+
+ + + +
{monitor.name}
+
+ +
+ + + +
+
{config.settings.daysInHistory} days ago
+
Today
+
+
+ ) + })} + +
+
+ ) +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..be229015fa3d5f0028a20a3aa1de1285fc7f45c2 GIT binary patch literal 15406 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 IVD!NM0bhA3CjbBd literal 0 HcmV?d00001 diff --git a/public/logo-192x192.png b/public/logo-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..e7c2fcd804286e39b59562c6a9c86947092fbd3b GIT binary patch literal 10205 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 X8a?U2J`JM600000NkvXXu0mjf!mdK8 literal 0 HcmV?d00001 diff --git a/public/main.css b/public/main.css new file mode 100644 index 000000000..223c31b73 --- /dev/null +++ b/public/main.css @@ -0,0 +1,66 @@ +body { + background: #eeeeee; +} + +.flex { + display: flex; + justify-content: center; + align-content: center; + align-items: center; +} +.flex.horizontal { + flex-direction: row; +} +.flex.vertical { + flex-direction: column; +} +.flex.between { + justify-content: space-between; +} +.marginless { + margin: 0 !important; +} +.paddingless { + padding: 0 !important; +} +.black-text { + color: #000 !important; +} +.grey-text { + color: #a0a0a0 !important; +} +.white-text { + color: #fff !important; +} +.histogram { + height: 24px; + width: 100%; + margin: 0 auto; +} +.hitbox { + align-items: flex-end; + box-sizing: border-box; + height: 100%; + width: 100%; + padding: 1px; + border-radius: 3.75px; +} +.bar { + background: #dcddde; + padding-bottom: 1px; + height: 100%; + width: 85%; + border-radius: 100px; +} +.bar.green { + background: #21ba45; +} +.bar.red { + background: #db2828; +} +.bar.orange { + background: #f2711c; +} +span i.icon { + margin: 0 !important; +} diff --git a/src/components/monitorHistogram.js b/src/components/monitorHistogram.js new file mode 100644 index 000000000..29c5108c0 --- /dev/null +++ b/src/components/monitorHistogram.js @@ -0,0 +1,49 @@ +import config from '../../config.yaml' + +export default function MonitorHistogram({ kvMonitorsDaysMap, monitor }) { + let date = new Date() + date.setDate(date.getDate() - config.settings.daysInHistory) + + if (typeof window !== 'undefined') { + return ( +
+ {Array.from(Array(config.settings.daysInHistory).keys()).map(key => { + date.setDate(date.getDate() + 1) + const dayInHistory = date.toISOString().split('T')[0] + const dayInHistoryKey = 'h_' + monitor.id + '_' + dayInHistory + + let bg = '' + let dayInHistoryStatus = 'No data' + + if (typeof kvMonitorsDaysMap[dayInHistoryKey] !== 'undefined') { + bg = kvMonitorsDaysMap[dayInHistoryKey] ? 'green' : 'orange' + dayInHistoryStatus = kvMonitorsDaysMap[dayInHistoryKey] + ? 'No outages' + : 'Some outages' + } + + return ( +
+
+
+ ) + })} +
+ ) + } else { + return ( +
+
Loading histogram ...
+
+ ) + } +} diff --git a/src/components/monitorStatusLabel.js b/src/components/monitorStatusLabel.js new file mode 100644 index 000000000..19b4957bf --- /dev/null +++ b/src/components/monitorStatusLabel.js @@ -0,0 +1,16 @@ +export default function MonitorStatusLabel({ kvMonitorsMap, monitor }) { + let labelColor = 'grey' + let labelText = 'No data' + + if (typeof kvMonitorsMap[monitor.id] !== 'undefined') { + if (kvMonitorsMap[monitor.id].operational) { + labelColor = 'green' + labelText = 'Operational' + } else { + labelColor = 'orange' + labelText = 'Not great not terrible' + } + } + + return
{labelText}
+} diff --git a/src/css/index.css b/src/css/index.css new file mode 100644 index 000000000..223c31b73 --- /dev/null +++ b/src/css/index.css @@ -0,0 +1,66 @@ +body { + background: #eeeeee; +} + +.flex { + display: flex; + justify-content: center; + align-content: center; + align-items: center; +} +.flex.horizontal { + flex-direction: row; +} +.flex.vertical { + flex-direction: column; +} +.flex.between { + justify-content: space-between; +} +.marginless { + margin: 0 !important; +} +.paddingless { + padding: 0 !important; +} +.black-text { + color: #000 !important; +} +.grey-text { + color: #a0a0a0 !important; +} +.white-text { + color: #fff !important; +} +.histogram { + height: 24px; + width: 100%; + margin: 0 auto; +} +.hitbox { + align-items: flex-end; + box-sizing: border-box; + height: 100%; + width: 100%; + padding: 1px; + border-radius: 3.75px; +} +.bar { + background: #dcddde; + padding-bottom: 1px; + height: 100%; + width: 85%; + border-radius: 100px; +} +.bar.green { + background: #21ba45; +} +.bar.red { + background: #db2828; +} +.bar.orange { + background: #f2711c; +} +span i.icon { + margin: 0 !important; +} diff --git a/src/functions/cronTrigger.js b/src/functions/cronTrigger.js new file mode 100644 index 000000000..4f6b2f722 --- /dev/null +++ b/src/functions/cronTrigger.js @@ -0,0 +1,57 @@ +import config from '../../config.yaml' + +import { setKV, getKV, getKVWithMetadata, gcMonitors } from './helpers' + +export async function processCronTrigger(event) { + for (const monitor of config.monitors) { + console.log(`Checking ${monitor.name} ...`) + + const init = { + method: monitor.method || 'GET', + redirect: monitor.followRedirect ? 'follow' : 'manual', + headers: { + 'User-Agent': 'cf-worker-status-page', + }, + } + + const response = await fetch(monitor.url, init) + const monitorOperational = response.status === (monitor.expectStatus || 200) + const kvMonitor = await getKVWithMetadata('s_' + monitor.id) + + // metadata from monitor settings + const metadata = { + operational: monitorOperational, + statusCode: response.status, + id: monitor.id, + } + + // write current status if status changed or for first time + if ( + !kvMonitor.metadata || + kvMonitor.metadata.operational !== monitorOperational + ) { + console.log('saving new results..') + + if (typeof SECRET_SLACK_WEBHOOK !== 'undefined') { + await notifySlack(metadata) + } + + await setKV('s_' + monitor.id, null, metadata) + } + + // check day status, write only on not operational or for first time + const kvDayStatusKey = + 'h_' + monitor.id + '_' + new Date().toISOString().split('T')[0] + //console.log(kvDayStatusKey) + const kvDayStatus = await getKV(kvDayStatusKey) + + if (!kvDayStatus || (kvDayStatus && !monitorOperational)) { + await setKV(kvDayStatusKey, null, metadata) + } + + await setKV('lastUpdate', Date.now()) + } + await gcMonitors(config) + + return new Response('OK') +} diff --git a/src/functions/helpers.js b/src/functions/helpers.js new file mode 100644 index 000000000..7a19e5425 --- /dev/null +++ b/src/functions/helpers.js @@ -0,0 +1,89 @@ +export async function getMonitors() { + const monitors = await listKV('s_') + return monitors.keys +} + +export async function getMonitorsHistory() { + const monitorsHistory = await listKV('h_', 600) + return monitorsHistory.keys +} + +export async function getLastUpdate() { + return await getKV('lastUpdate') +} + +export async function listKV(prefix = '', cacheTtl = false) { + const cacheKey = 'list_' + prefix + '_' + process.env.BUILD_ID + const cachedResponse = await getKV(cacheKey) + + if (cacheTtl && cachedResponse) { + return JSON.parse(cachedResponse) + } + + let list = [] + let cursor = null + let res = {} + do { + res = await KV_STATUS_PAGE.list({ prefix: prefix, cursor }) + list = list.concat(res.keys) + cursor = res.cursor + } while (!res.list_complete) + + if (cacheTtl) { + await setKV(cacheKey, JSON.stringify({ keys: list }), null, 600) + } + return { keys: list } +} + +export async function setKV(key, value, metadata, expirationTtl) { + return KV_STATUS_PAGE.put(key, value, { metadata, expirationTtl }) +} + +export async function getKV(key, type = 'text') { + return KV_STATUS_PAGE.get(key, type) +} + +export async function getKVWithMetadata(key) { + return KV_STATUS_PAGE.getWithMetadata(key) +} + +export async function deleteKV(key) { + return KV_STATUS_PAGE.delete(key) +} + +export async function gcMonitors(config) { + const checkKvPrefix = 's_' + + const monitors = config.monitors.map(key => { + return key.id + }) + + const kvMonitors = await listKV(checkKvPrefix) + const kvState = kvMonitors.keys.map(key => { + return key.metadata.id + }) + + const keysForRemoval = kvState.filter(x => !monitors.includes(x)) + + keysForRemoval.forEach(key => { + console.log('gc: deleting ' + checkKvPrefix + key) + deleteKV(checkKvPrefix + key) + }) +} + +async function notifySlack(monitor, metadata) { + const blocks = [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `Some monitor is now in :this: status`, + }, + }, + ] + return fetch(SECRET_SLACK_WEBHOOK_URL, { + body: JSON.stringify({ blocks }), + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }) +} diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 000000000..14a6c2994 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,23 @@ +name = "cf-workers-status-page" +type = "webpack" +account_id = "" +workers_dev = true +route = "" +zone_id = "" +webpack_config = "node_modules/flareact/webpack" + +# uncomment and adjust following if you are not using GitHub Actions +# kv_namespaces = [{binding="KV_GITHUB_RELEASES", id="xxxx"}] +# preview_id = "9581809385634861ae93b0e01677b44d" + +# delete afterwards +kv-namespaces = [ + { binding = "KV_STATUS_PAGE", id = "c27344947ebb476880fa2ba0ef9bbd10", preview_id = "c27344947ebb476880fa2ba0ef9bbd10" } +] + +[triggers] +crons = ["* * * * *"] + +[site] +bucket = "out" +entry-point = "./"