Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 33d15436d3 | |||
| f419b8ad2b | |||
| 8d704ef44c | |||
| 7f58e2673a | |||
| 7813ff93ac | |||
| 596d30389f | |||
| 1189e708da | |||
| de0cfaf504 | |||
| f0b27a8446 | |||
| a8cfe25c9a | |||
| df64cdc113 | |||
| d87223f292 | |||
| a5a65a7582 | |||
| 23673587c2 | |||
| fa9d865cc1 | |||
| 0fac88d7f8 | |||
| 7a9a2f90ef | |||
| c337b55c37 | |||
| f0a4974e92 | |||
| cb15831994 | |||
| 2868f9820c | |||
| 48d78d117a | |||
| c7f642cd8c | |||
| 54bdd42fc1 | |||
| bbf9def61f | |||
| ccec34ff5d | |||
| ee586c9e64 | |||
| 8d70a0f992 | |||
| 6952ed2e8d | |||
| 6d08f47d1d | |||
| 1439b67c30 | |||
| 100473fc58 | |||
| 7baab0475e | |||
| c90ee97d3a | |||
| c13095a2df | |||
| 6ab6e38d56 |
@@ -0,0 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: eidam
|
||||
ko_fi: eidam
|
||||
@@ -5,6 +5,8 @@ on:
|
||||
branches:
|
||||
- main
|
||||
repository_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 1 * *'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -17,6 +19,8 @@ jobs:
|
||||
node-version: 12
|
||||
- run: yarn install
|
||||
- run: yarn build
|
||||
env:
|
||||
NODE_ENV: production
|
||||
- name: Publish
|
||||
uses: cloudflare/wrangler-action@1.3.0
|
||||
with:
|
||||
@@ -30,16 +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:
|
||||
NODE_ENV: production
|
||||
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}}
|
||||
|
||||
@@ -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`_
|
||||
|
||||
+3
-1
@@ -3,7 +3,7 @@ settings:
|
||||
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
|
||||
collectResponseTimes: false # experimental feature, enable only for <5 monitors or on paid plans
|
||||
collectResponseTimes: true # collects avg response times from CRON locations
|
||||
|
||||
allmonitorsOperational: 'All Systems Operational'
|
||||
notAllmonitorsOperational: 'Not All Systems Operational'
|
||||
@@ -22,6 +22,7 @@ monitors:
|
||||
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
|
||||
@@ -29,6 +30,7 @@ monitors:
|
||||
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
|
||||
|
||||
+4
-7
@@ -11,11 +11,10 @@
|
||||
"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",
|
||||
"postinstall": "patch-package"
|
||||
"css": "postcss public/tailwind.css -o public/style.css"
|
||||
},
|
||||
"dependencies": {
|
||||
"flareact": "0.9.0",
|
||||
"flareact": "^0.10.0",
|
||||
"laco": "^1.2.1",
|
||||
"laco-react": "^1.1.0",
|
||||
"react": "^17.0.1",
|
||||
@@ -24,12 +23,10 @@
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.0.2",
|
||||
"node-fetch": "^2.6.1",
|
||||
"postcss": "^8.1.8",
|
||||
"postcss": "^8.2.13",
|
||||
"postcss-cli": "^8.3.0",
|
||||
"prettier": "^2.2.0",
|
||||
"tailwindcss": "^2.0.1",
|
||||
"yaml-loader": "^0.6.0",
|
||||
"patch-package": "^6.2.2",
|
||||
"postinstall-postinstall": "^2.1.0"
|
||||
"yaml-loader": "^0.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
diff --git a/node_modules/flareact/src/components/_document.js b/node_modules/flareact/src/components/_document.js
|
||||
index 3494b60..206b493 100644
|
||||
--- a/node_modules/flareact/src/components/_document.js
|
||||
+++ b/node_modules/flareact/src/components/_document.js
|
||||
@@ -61,6 +61,7 @@ export function FlareactHead({ helmet, page, buildManifest }) {
|
||||
{helmet.title.toComponent()}
|
||||
{helmet.meta.toComponent()}
|
||||
{helmet.link.toComponent()}
|
||||
+ {helmet.script.toComponent()}
|
||||
|
||||
{[...links].map((link) => (
|
||||
<link href={`/_flareact/static/${link}`} rel="stylesheet" />
|
||||
+37
-24
@@ -6,7 +6,7 @@ const accountId = process.env.CF_ACCOUNT_ID
|
||||
const namespaceId = process.env.KV_NAMESPACE_ID
|
||||
const apiToken = process.env.CF_API_TOKEN
|
||||
|
||||
const kvPrefix = 's_'
|
||||
const kvMonitorsKey = 'monitors_data_v1_1'
|
||||
|
||||
if (!accountId || !namespaceId || !apiToken) {
|
||||
console.error(
|
||||
@@ -15,7 +15,7 @@ if (!accountId || !namespaceId || !apiToken) {
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
async function getKvMonitors(kvPrefix) {
|
||||
async function getKvMonitors(kvMonitorsKey) {
|
||||
const init = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -24,27 +24,29 @@ async function getKvMonitors(kvPrefix) {
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/keys?limit=100&prefix=${kvPrefix}`,
|
||||
`https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${kvMonitorsKey}`,
|
||||
init,
|
||||
)
|
||||
const json = await res.json()
|
||||
return json.result
|
||||
return json
|
||||
}
|
||||
|
||||
async function deleteKvBulk(keys) {
|
||||
async function saveKVMonitors(kvMonitorsKey, data) {
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
},
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(keys),
|
||||
body: JSON.stringify(data),
|
||||
}
|
||||
|
||||
return await fetch(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/bulk`,
|
||||
const res = await fetch(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${kvMonitorsKey}`,
|
||||
init,
|
||||
)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
@@ -53,26 +55,37 @@ function loadConfig() {
|
||||
return JSON.parse(config)
|
||||
}
|
||||
|
||||
getKvMonitors(kvPrefix)
|
||||
getKvMonitors(kvMonitorsKey)
|
||||
.then(async (kvMonitors) => {
|
||||
let stateMonitors = kvMonitors
|
||||
|
||||
const config = loadConfig()
|
||||
const monitors = config.monitors.map((key) => {
|
||||
const configMonitors = config.monitors.map((key) => {
|
||||
return key.id
|
||||
})
|
||||
const kvState = kvMonitors.map((key) => {
|
||||
return key.name
|
||||
})
|
||||
const keysForRemoval = kvState.filter(
|
||||
(x) => !monitors.includes(x.replace(kvPrefix, '')),
|
||||
)
|
||||
|
||||
if (keysForRemoval.length > 0) {
|
||||
console.log(
|
||||
`Removing following keys from KV storage as they are no longer in the config: ${keysForRemoval.join(
|
||||
', ',
|
||||
)}`,
|
||||
)
|
||||
await deleteKvBulk(keysForRemoval)
|
||||
Object.keys(stateMonitors.monitors).map((monitor) => {
|
||||
// remove monitor data from state if missing in config
|
||||
if (!configMonitors.includes(monitor)) {
|
||||
delete stateMonitors.monitors[monitor]
|
||||
}
|
||||
|
||||
// delete dates older than config.settings.daysInHistogram
|
||||
let date = new Date()
|
||||
date.setDate(date.getDate() - config.settings.daysInHistogram)
|
||||
date.toISOString().split('T')[0]
|
||||
const cleanUpDate = date.toISOString().split('T')[0]
|
||||
|
||||
Object.keys(stateMonitors.monitors[monitor].checks).map((checkDay) => {
|
||||
if (checkDay < cleanUpDate) {
|
||||
delete stateMonitors.monitors[monitor].checks[checkDay]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// sanity check + if good save the KV
|
||||
if (configMonitors.length === Object.keys(stateMonitors.monitors).length) {
|
||||
await saveKVMonitors(kvMonitorsKey, stateMonitors)
|
||||
}
|
||||
})
|
||||
.catch((e) => console.log(e))
|
||||
|
||||
@@ -30,7 +30,20 @@ export default function MonitorCard({ key, monitor, data }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xl">{monitor.name}</div>
|
||||
{(monitor.linkable === true || monitor.linkable === undefined) ?
|
||||
(
|
||||
<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} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { locations } from '../functions/locations'
|
||||
|
||||
export default function MonitorDayAverage({ location, avg }) {
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
<small>
|
||||
{locations[location] || location}: {avg}ms
|
||||
</small>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react'
|
||||
import config from '../../config.yaml'
|
||||
import MonitorDayAverage from './monitorDayAverage'
|
||||
|
||||
export default function MonitorHistogram({ monitorId, kvMonitor }) {
|
||||
// create date and set date - daysInHistogram for the first day of the histogram
|
||||
@@ -43,10 +45,10 @@ export default function MonitorHistogram({ monitorId, kvMonitor }) {
|
||||
kvMonitor.checks.hasOwnProperty(dayInHistogram) &&
|
||||
Object.keys(kvMonitor.checks[dayInHistogram].res).map((key) => {
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
{key}: {kvMonitor.checks[dayInHistogram].res[key].a}ms
|
||||
</>
|
||||
<MonitorDayAverage
|
||||
location={key}
|
||||
avg={kvMonitor.checks[dayInHistogram].res[key].a}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import config from '../../config.yaml'
|
||||
import { locations } from '../functions/locations'
|
||||
|
||||
const classes = {
|
||||
green:
|
||||
@@ -24,7 +25,8 @@ export default function MonitorStatusHeader({ kvMonitorsLastUpdate }) {
|
||||
<div className="text-xs font-light">
|
||||
checked{' '}
|
||||
{Math.round((Date.now() - kvMonitorsLastUpdate.time) / 1000)} sec
|
||||
ago (from {kvMonitorsLastUpdate.loc})
|
||||
ago (from{' '}
|
||||
{locations[kvMonitorsLastUpdate.loc] || kvMonitorsLastUpdate.loc})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getCheckLocation,
|
||||
getKVMonitors,
|
||||
setKVMonitors,
|
||||
notifyDiscord,
|
||||
} from './helpers'
|
||||
|
||||
function getDate() {
|
||||
@@ -88,6 +89,15 @@ export async function processCronTrigger(event) {
|
||||
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) &&
|
||||
@@ -131,8 +141,8 @@ export async function processCronTrigger(event) {
|
||||
// Save allOperational to false
|
||||
monitorsState.lastUpdate.allOperational = false
|
||||
|
||||
// Increment failed checks, only on status change (maybe call it .incidents instead?)
|
||||
if (monitorStatusChanged) {
|
||||
// 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++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export async function notifySlack(monitor, operational) {
|
||||
const payload = {
|
||||
attachments: [
|
||||
{
|
||||
fallback: `Monitor ${monitor.name} changed status to ${getOperationalLabel(operational)}`,
|
||||
color: operational ? '#36a64f' : '#f2c744',
|
||||
blocks: [
|
||||
{
|
||||
@@ -63,7 +64,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)}*
|
||||
@@ -83,6 +84,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)
|
||||
|
||||
|
||||
@@ -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. George’s',
|
||||
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: 'Xi’an',
|
||||
YUL: 'Montréal',
|
||||
YVR: 'Vancouver',
|
||||
YWG: 'Winnipeg',
|
||||
YXE: 'Saskatoon',
|
||||
YYC: 'Calgary',
|
||||
YYZ: 'Toronto',
|
||||
ZAG: 'Zagreb',
|
||||
ZDM: 'Ramallah ',
|
||||
ZRH: 'Zürich',
|
||||
}
|
||||
@@ -3,6 +3,7 @@ workers_dev = true
|
||||
account_id = ""
|
||||
type = "webpack"
|
||||
webpack_config = "node_modules/flareact/webpack"
|
||||
compatibility_date = "2021-07-23"
|
||||
|
||||
[triggers]
|
||||
crons = ["* * * * *"]
|
||||
|
||||
Reference in New Issue
Block a user