Compare commits
No commits in common. "main" and "next" have entirely different histories.
27
.github/workflows/main.yml
vendored
|
@ -1,9 +1,13 @@
|
|||
name: Create and publish a Docker image
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
repository_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
|
@ -19,8 +23,6 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v1
|
||||
|
@ -34,28 +36,11 @@ jobs:
|
|||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4.0.0
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
- uses: paulhatch/semantic-version@v5.0.2
|
||||
id: vnum
|
||||
with:
|
||||
# The prefix to use to identify tags
|
||||
tag_prefix: ""
|
||||
major_pattern: "(MAJOR)"
|
||||
minor_pattern: "(MINOR)"
|
||||
version_format: "${major}.${minor}.${patch}-${increment}"
|
||||
bump_each_commit: true
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vnum.outputs.version_tag }}
|
||||
${{ steps.meta.outputs.tags }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
|
25
.gitignore
vendored
|
@ -1,2 +1,23 @@
|
|||
/target
|
||||
.env
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
|
8
.idea/modules.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/nws-site.iml" filepath="$PROJECT_DIR$/.idea/nws-site.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
12
.idea/nws-site.iml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
.idea/vcs.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
90
.idea/workspace.xml
Normal file
|
@ -0,0 +1,90 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="03fafda4-e2c1-4602-a731-a2f96e84badd" name="Default Changelist" comment="">
|
||||
<change afterPath="$PROJECT_DIR$/src/components/IncidentCard.tsx" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/components/UptimeCard.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/App.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/App.tsx" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="FileTemplateManagerImpl">
|
||||
<option name="RECENT_TEMPLATES">
|
||||
<list>
|
||||
<option value="TypeScript File" />
|
||||
<option value="TypeScript JSX File" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="MarkdownSettingsMigration">
|
||||
<option name="stateVersion" value="1" />
|
||||
</component>
|
||||
<component name="ProjectId" id="24cZXAEHtOTkBPpX5NH0IuXFSFd" />
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">
|
||||
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
|
||||
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
|
||||
<property name="WebServerToolWindowFactoryState" value="false" />
|
||||
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
|
||||
<property name="node.js.detected.package.eslint" value="true" />
|
||||
<property name="node.js.path.for.package.eslint" value="project" />
|
||||
<property name="node.js.selected.package.eslint" value="(autodetect)" />
|
||||
<property name="nodejs_interpreter_path" value="/usr/local/bin/node" />
|
||||
<property name="nodejs_package_manager_path" value="npm" />
|
||||
<property name="ts.external.directory.path" value="$PROJECT_DIR$/node_modules/typescript/lib" />
|
||||
<property name="vue.rearranger.settings.migration" value="true" />
|
||||
</component>
|
||||
<component name="RecentsManager">
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$/src/static/images" />
|
||||
</key>
|
||||
</component>
|
||||
<component name="RunManager">
|
||||
<configuration name="Debug Application" type="JavascriptDebugType" uri="http://localhost:3000">
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="03fafda4-e2c1-4602-a731-a2f96e84badd" name="Default Changelist" comment="" />
|
||||
<created>1643931872831</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1643931872831</updated>
|
||||
<workItem from="1643931873891" duration="52000" />
|
||||
<workItem from="1643931940066" duration="3064000" />
|
||||
<workItem from="1643936709048" duration="547000" />
|
||||
<workItem from="1648500425230" duration="246000" />
|
||||
<workItem from="1658028513357" duration="2964000" />
|
||||
<workItem from="1666469240565" duration="7361000" />
|
||||
<workItem from="1666543043382" duration="439000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="Vcs.Log.Tabs.Properties">
|
||||
<option name="TAB_STATES">
|
||||
<map>
|
||||
<entry key="MAIN">
|
||||
<value>
|
||||
<State />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
2122
Cargo.lock
generated
24
Cargo.toml
|
@ -1,24 +0,0 @@
|
|||
[package]
|
||||
name = "website"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
askama = { version = "0.12.1", features = ["with-axum"] }
|
||||
askama_axum = "0.4.0"
|
||||
axum = "0.7.4"
|
||||
tower-http = { version = "0.5.1", features = ["fs", "trace"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
reqwest = { version = "0.12.4", features = ["json"] }
|
||||
serde = "1.0.201"
|
||||
serde_json = "1.0.117"
|
||||
chrono = "0.4.38"
|
||||
rust_decimal = "1.35.0"
|
||||
anyhow = "1.0.83"
|
||||
env_logger = "0.11.3"
|
||||
log = "0.4.21"
|
||||
dotenv = "0.15.0"
|
||||
dotenv_codegen = "0.15.0"
|
||||
lazy_static = "1.4.0"
|
26
Dockerfile
|
@ -1,14 +1,18 @@
|
|||
FROM rust:1.74.1 as build
|
||||
# build environment
|
||||
FROM node:13.12.0-alpine as build
|
||||
WORKDIR /app
|
||||
ENV PATH /app/node_modules/.bin:$PATH
|
||||
COPY package.json ./
|
||||
COPY package-lock.json ./
|
||||
RUN npm install
|
||||
|
||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||
|
||||
WORKDIR .
|
||||
COPY . .
|
||||
|
||||
RUN cargo install --path .
|
||||
|
||||
ENV RUST_LOG=info
|
||||
ENV EXPOSE_PORT=80
|
||||
RUN npm install react-scripts@3.4.1 -g --silent
|
||||
COPY . ./
|
||||
RUN npm run build
|
||||
|
||||
# production environment
|
||||
FROM nginx:stable-alpine
|
||||
COPY --from=build /app/build /usr/share/nginx/html
|
||||
COPY --from=build /app/nginx/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
ENTRYPOINT ["website"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
|
10
README.md
|
@ -1 +1,9 @@
|
|||
# Nick Web Services Web(site/API)
|
||||
<img src="./src/static/images/NWS_Logo.png" alt="NWS Logo" width="200"/>
|
||||
|
||||
# nws-site
|
||||
The offical website for Nick Web Services (aka NWS)
|
||||
|
||||
## Features
|
||||
- Shows uptime of NWS servers
|
||||
- Running on over-engineered infrastructure
|
||||
- Very simple yet effective design
|
||||
|
|
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 220 B |
Before Width: | Height: | Size: 476 B |
|
@ -1,45 +0,0 @@
|
|||
* {
|
||||
font-family: serif;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #ccf2b3; /* #ffed8f; */
|
||||
margin: 10px auto;
|
||||
max-width: 750px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #114488;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.flag-img {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
p.lineitem::after {
|
||||
content: " ";
|
||||
flex: 1;
|
||||
border-bottom: 1px dotted #000;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
white-space: nowrap;
|
||||
}
|
10
makefile
|
@ -1,10 +0,0 @@
|
|||
.PHONY: run
|
||||
|
||||
run:
|
||||
RUST_LOG=debug cargo run
|
||||
|
||||
docker-build:
|
||||
docker build . -t smc-website:dev
|
||||
|
||||
docker-run: docker-build
|
||||
docker run -p 8085:80 smc-website:dev
|
30
nginx/nginx.conf
Normal file
|
@ -0,0 +1,30 @@
|
|||
server {
|
||||
gzip on;
|
||||
gzip_types
|
||||
application/atom+xml
|
||||
application/geo+json
|
||||
application/javascript
|
||||
application/x-javascript
|
||||
application/json
|
||||
application/ld+json
|
||||
application/manifest+json
|
||||
application/rdf+xml
|
||||
application/rss+xml
|
||||
application/xhtml+xml
|
||||
application/xml
|
||||
font/eot
|
||||
font/otf
|
||||
font/ttf
|
||||
image/svg+xml
|
||||
text/css
|
||||
text/javascript
|
||||
text/plain
|
||||
text/xml;
|
||||
gzip_min_length 256;
|
||||
listen 80;
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
}
|
||||
}
|
27806
package-lock.json
generated
Normal file
50
package.json
Normal file
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"name": "nws-site",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.2",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/node": "^16.11.22",
|
||||
"@types/react": "^17.0.39",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"bootstrap": "^5.1.3",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^2.4.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "5.0.0",
|
||||
"react-tooltip": "^4.4.3",
|
||||
"typescript": "^4.5.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "17.0.2",
|
||||
"@types/react-dom": "17.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 3.8 KiB |
42
public/index.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Nick Web Services - The best cloud compute platform"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>NWS</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
BIN
public/logo192.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
25
public/manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
34
src/App.css
Normal file
|
@ -0,0 +1,34 @@
|
|||
.App {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.low-severity {
|
||||
background-color: #98fb98
|
||||
}
|
||||
|
||||
.med-severity {
|
||||
background-color: #eee8aa
|
||||
}
|
||||
|
||||
.high-severity {
|
||||
background-color: #f08080
|
||||
}
|
||||
|
||||
.severity-label {
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.nws-card {
|
||||
border-radius: 20px;
|
||||
background-color: #eee;
|
||||
padding: .75rem;
|
||||
}
|
||||
|
||||
|
9
src/App.test.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
98
src/App.tsx
Normal file
|
@ -0,0 +1,98 @@
|
|||
import React, {useEffect, useState} from 'react';
|
||||
import NWSLogo from './static/images/NWS_Logo.png';
|
||||
import './App.css';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import {Incident, UptimeResponse} from "./nws-api/types";
|
||||
import {getIncidents, getUptime} from "./nws-api/calls";
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import UptimeCard from "./components/UptimeCard";
|
||||
import IncidentCard from "./components/IncidentCard";
|
||||
|
||||
function App() {
|
||||
|
||||
const [uptime, setUptime] = useState<UptimeResponse>({datacenters: [], services:[], lastUpdated: ""});
|
||||
const [incidents, setIncidents] = useState<Incident[]>([]);
|
||||
|
||||
const fetchUptime = async () => {
|
||||
let resp: UptimeResponse = await getUptime();
|
||||
setUptime(resp);
|
||||
}
|
||||
|
||||
const fetchIncidents = async () => {
|
||||
let resp: Incident[] = await getIncidents();
|
||||
setIncidents(resp);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchUptime();
|
||||
fetchIncidents();
|
||||
}, []);
|
||||
|
||||
// @ts-ignore
|
||||
return (
|
||||
<div className="App">
|
||||
|
||||
|
||||
<div className={"w-100 row"}>
|
||||
<div className={"col-md-6 d-flex justify-content-center flex-column align-items-center"}>
|
||||
<img src={NWSLogo} alt="nws-logo" style={{width: "70%"}}/>
|
||||
</div>
|
||||
<div className={"col-md-6 text-center d-flex justify-content-center flex-column align-items-center"}>
|
||||
<h1>Nick Web Services</h1>
|
||||
<p style={{ maxWidth: 500 }} className={"col-md-6 text-center"}>
|
||||
Nick Web Services is a hosting service based out of
|
||||
Austin, Texas. It is committed
|
||||
to achieving maximum uptime with better performance and a lower cost than any of the major cloud
|
||||
services.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{width: '75vw'}}>
|
||||
<hr/>
|
||||
</div>
|
||||
|
||||
<div className={"text-left row"} style={{width: '75vw'}}>
|
||||
<h2>NWS System Status</h2>
|
||||
<p>Last updated at {new Date(uptime.lastUpdated).toLocaleString()}</p>
|
||||
<div className={"col-md-6 col-12"}>
|
||||
<h3>Service Status</h3>
|
||||
{uptime.services.map((e) => {
|
||||
return (
|
||||
<UptimeCard uptime={e}/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={"col-md-6 col-12"}>
|
||||
<h3>Datacenter Status</h3>
|
||||
{uptime.datacenters.map((e) => {
|
||||
return (
|
||||
<UptimeCard uptime={e}/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{width: '75vw'}}>
|
||||
<hr/>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Service Alerts</h3>
|
||||
{incidents.map((e) => {
|
||||
return (
|
||||
<IncidentCard incident={e}/>
|
||||
);
|
||||
})}
|
||||
{incidents.length == 0 && <div className={`row text-center`} style={{width: '75vw'}}>
|
||||
<h5 className={"col-12"}>No service alerts.</h5>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
|
||||
<footer style={{margin: 25}}>
|
||||
NWS is owned and operated by <a href={"http://nickorlow.com"}>Nicholas Orlowsky</a>.
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
30
src/components/IncidentCard.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import {Incident} from "../nws-api/types";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import React from "react";
|
||||
|
||||
export default function IncidentCard(props: {incident: Incident}) {
|
||||
|
||||
const severityText: string[] = [
|
||||
"Low Severity means that this issue does not affect any services running on NWS.",
|
||||
"Medium Severity means that this issue may cause some slowdowns or outages on some services.",
|
||||
"High Severity means that this issue causes an outage on the entire NWS network or most of the services running on it."
|
||||
];
|
||||
let severityClass: string = props.incident.severity == 0 ? 'low' : (props.incident.severity == 1 ? 'med' : 'high');
|
||||
let severityString: string = props.incident.severity == 0 ? 'Low' : (props.incident.severity == 1 ? 'Medium' : 'High');
|
||||
|
||||
return (
|
||||
<div className={`row text-left nws-card`} style={{width: '75vw'}}>
|
||||
<p className={"col-md-10 col-12 mb-2"}><b>{props.incident.title}</b></p>
|
||||
<div className={`col-md-2 col-12 mb-2`}>
|
||||
<div className={`severity-label w-max-content ${severityClass}-severity`}
|
||||
data-tip={severityText[props.incident.severity]}>
|
||||
<b>
|
||||
{severityString} Severity ⓘ
|
||||
</b>
|
||||
<ReactTooltip />
|
||||
</div>
|
||||
</div>
|
||||
<p className={"mb-0"}>{props.incident.description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
29
src/components/UptimeCard.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import {UptimeRecord} from "../nws-api/types";
|
||||
import React from "react";
|
||||
import '../App.css';
|
||||
|
||||
export default function UptimeCard(props: {uptime: UptimeRecord}) {
|
||||
return(
|
||||
<div className={"nws-card row mb-2"} style={{maxWidth: '100%'}}>
|
||||
{props.uptime.url != null && <h3 className={"col-md-9 col-12"}><a href={props.uptime.url} style={{textDecoration: "none"}}>{props.uptime.name}</a></h3>}
|
||||
{props.uptime.url == null && <h3 className={"col-md-9 col-12"}>{props.uptime.name}</h3>}
|
||||
|
||||
<div className={`col-md-3 col-12 d-flex d-md-none justify-content-start`}>
|
||||
<p className={`fw-bold severity-label
|
||||
${props.uptime.isUp ? 'low' : (props.uptime.undergoingMaintenance ? 'med' : 'high')}-severity`}
|
||||
style={{width: "max-content"}}>
|
||||
{props.uptime.isUp ? 'Up' : (props.uptime.undergoingMaintenance ? 'Maintenance' : 'Down')}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`d-md-flex col-md-3 col-12 d-none justify-content-end`}>
|
||||
<p className={`fw-bold severity-label
|
||||
${props.uptime.isUp ? 'low' : (props.uptime.undergoingMaintenance ? 'med' : 'high')}-severity`}
|
||||
style={{width: "max-content"}}>
|
||||
{props.uptime.isUp ? 'Up' : (props.uptime.undergoingMaintenance ? 'Maintenance' : 'Down')}
|
||||
</p>
|
||||
</div>
|
||||
<p className={"col-md-12 col-12"}><b>Last Month Uptime:</b> {props.uptime.uptimeMonth}%</p>
|
||||
<p className={"col-md-6 col-12"}><b>All Time Uptime:</b> {props.uptime.uptimeAllTime}%</p>
|
||||
</div>
|
||||
);
|
||||
}
|
13
src/index.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
17
src/index.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
1
src/logo.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
After Width: | Height: | Size: 2.6 KiB |
272
src/main.rs
|
@ -1,272 +0,0 @@
|
|||
use axum::{routing::get, Router};
|
||||
use chrono::offset::Utc;
|
||||
use chrono::DateTime;
|
||||
use log::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[macro_use]
|
||||
extern crate dotenv_codegen;
|
||||
extern crate dotenv;
|
||||
use dotenv::dotenv;
|
||||
use std::env;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
mod uptime_service;
|
||||
|
||||
use uptime_service::{Uptime, UptimeService, UptimeStatus, UptimeType};
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "layout.html")]
|
||||
struct ContentTemplate<T: askama::Template> {
|
||||
content: T,
|
||||
page_title: Option<String>,
|
||||
page_desc: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "layout.html")]
|
||||
struct RawContentTemplate {
|
||||
content: String,
|
||||
page_title: Option<String>,
|
||||
page_desc: Option<String>,
|
||||
}
|
||||
|
||||
struct UptimeInfo {
|
||||
name: String,
|
||||
uptime: String,
|
||||
response_time: String,
|
||||
status: String,
|
||||
url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "index.html")]
|
||||
struct IndexTemplate {
|
||||
uptime_infos: Vec<UptimeInfo>,
|
||||
last_updated: String,
|
||||
}
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "system_status.html")]
|
||||
struct StatusTemplate {
|
||||
dctr_uptime_infos: Vec<UptimeInfo>,
|
||||
svc_uptime_infos: Vec<UptimeInfo>,
|
||||
last_updated: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct BlogInfo<'a> {
|
||||
title: &'a str,
|
||||
date: &'a str,
|
||||
url: &'a str,
|
||||
}
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "blog.html")]
|
||||
struct BlogTemplate<'a> {
|
||||
blogs: Vec<BlogInfo<'a>>,
|
||||
}
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "dashboard.html")]
|
||||
struct DashboardTemplate {}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
uptime_service: UptimeService,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
dotenv().ok();
|
||||
env_logger::init();
|
||||
|
||||
info!("Starting Sharpe Mountain Compute Website");
|
||||
|
||||
let uptime_service: UptimeService = UptimeService::new();
|
||||
uptime_service.start();
|
||||
|
||||
let state = AppState { uptime_service };
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(index_handler))
|
||||
.route("/system_status", get(status_handler))
|
||||
.route("/dashboard", get(dashboard_handler))
|
||||
.route("/blog", get(blog_handler))
|
||||
.route("/blogs/:blog_name", get(single_blog_handler))
|
||||
.nest_service("/assets", tower_http::services::ServeDir::new("assets"))
|
||||
.with_state(state);
|
||||
|
||||
let port_num = env::var("EXPOSE_PORT").unwrap_or("3000".to_string());
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port_num))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
info!("Listening on port {}", port_num);
|
||||
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
|
||||
lazy_static! {
|
||||
static ref blogs: HashMap<&'static str, BlogInfo<'static>> = {
|
||||
let mut m = HashMap::new();
|
||||
m.insert(
|
||||
"11-28-2024-onward-postmortem",
|
||||
BlogInfo {
|
||||
title: "Downtime Incident Postmortem (Nov 2024 - Present)",
|
||||
date: "December 28th, 2024",
|
||||
url: "11-28-2023-postmortem",
|
||||
},
|
||||
);
|
||||
m.insert(
|
||||
"11-08-2023-postmortem",
|
||||
BlogInfo {
|
||||
title: "Downtime Incident Postmortem (Nov 2023)",
|
||||
date: "November 11th, 2023",
|
||||
url: "11-08-2023-postmortem",
|
||||
},
|
||||
);
|
||||
m.insert(
|
||||
"ssl-on-cds",
|
||||
BlogInfo {
|
||||
title: "SSL on Container Deployment Service (at nickorlow.com)",
|
||||
date: "July 12th, 2023",
|
||||
url: "https://nickorlow.com/blogs/side-project-7-12-23.html",
|
||||
},
|
||||
);
|
||||
m
|
||||
};
|
||||
}
|
||||
|
||||
async fn blog_handler(
|
||||
) -> Result<ContentTemplate<impl askama::Template>, (axum::http::StatusCode, String)> {
|
||||
Ok(ContentTemplate {
|
||||
page_title: Some("NWS | Blog".to_string()),
|
||||
page_desc: Some("Read about the engineering behind NWS.".to_string()),
|
||||
content: BlogTemplate {
|
||||
blogs: blogs.values().cloned().collect::<Vec<BlogInfo>>()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn single_blog_handler(
|
||||
axum::extract::Path((blog_name)): axum::extract::Path<(String)>,
|
||||
) -> Result<RawContentTemplate, (axum::http::StatusCode, String)> {
|
||||
let blog_content = match std::fs::read_to_string(format!("templates/blogs/{}.html", blog_name))
|
||||
{
|
||||
Ok(ctn) => ctn,
|
||||
_ => String::from("<h1>Not Found!</h1>"),
|
||||
};
|
||||
Ok(RawContentTemplate {
|
||||
page_title: Some("NWS | Blog Post".to_string()),
|
||||
page_desc: Some("A Nick Web Services Blog Post.".to_string()),
|
||||
content: blog_content,
|
||||
})
|
||||
}
|
||||
|
||||
async fn dashboard_handler(
|
||||
) -> Result<ContentTemplate<impl askama::Template>, (axum::http::StatusCode, String)> {
|
||||
Ok(ContentTemplate {
|
||||
page_title: Some("NWS | Dashboard".to_string()),
|
||||
page_desc: Some("Manage the services you have deployed on NWS.".to_string()),
|
||||
content: DashboardTemplate {},
|
||||
})
|
||||
}
|
||||
|
||||
async fn index_handler(
|
||||
axum::extract::State(state): axum::extract::State<AppState>,
|
||||
) -> Result<ContentTemplate<impl askama::Template>, (axum::http::StatusCode, String)> {
|
||||
let uptimes: Vec<Uptime> = state.uptime_service.get_data();
|
||||
let lu: DateTime<Utc> = state.uptime_service.get_last_updated().into();
|
||||
let lu_str = format!("{} UTC", lu.format("%B %e, %Y %T"));
|
||||
|
||||
let mut uptime_infos: Vec<UptimeInfo> = vec![];
|
||||
|
||||
for uptime in uptimes {
|
||||
if uptime.uptime_type != UptimeType::Provider {
|
||||
continue;
|
||||
}
|
||||
|
||||
uptime_infos.push(UptimeInfo {
|
||||
name: uptime.name,
|
||||
uptime: uptime.uptime,
|
||||
response_time: uptime.response_time,
|
||||
status: match uptime.status {
|
||||
UptimeStatus::Up => String::from("Up"),
|
||||
UptimeStatus::Down => String::from("DOWN"),
|
||||
UptimeStatus::Maintenance => String::from("Undergoing Maintenance"),
|
||||
_ => String::from("Unknown"),
|
||||
},
|
||||
url: None,
|
||||
});
|
||||
}
|
||||
|
||||
let index_template = IndexTemplate {
|
||||
uptime_infos,
|
||||
last_updated: lu_str,
|
||||
};
|
||||
Ok(ContentTemplate {
|
||||
page_title: None,
|
||||
page_desc: None,
|
||||
content: index_template,
|
||||
})
|
||||
}
|
||||
|
||||
async fn status_handler(
|
||||
axum::extract::State(state): axum::extract::State<AppState>,
|
||||
) -> Result<ContentTemplate<impl askama::Template>, (axum::http::StatusCode, String)> {
|
||||
let uptimes: Vec<Uptime> = state.uptime_service.get_data();
|
||||
let lu: DateTime<Utc> = state.uptime_service.get_last_updated().into();
|
||||
let lu_str = format!("{} UTC", lu.format("%B %e, %Y %T"));
|
||||
|
||||
let mut dc_uptime_infos: Vec<UptimeInfo> = vec![];
|
||||
let mut sv_uptime_infos: Vec<UptimeInfo> = vec![];
|
||||
|
||||
for uptime in uptimes {
|
||||
match uptime.uptime_type {
|
||||
UptimeType::Datacenter => {
|
||||
dc_uptime_infos.push(UptimeInfo {
|
||||
name: uptime.name,
|
||||
uptime: uptime.uptime,
|
||||
response_time: uptime.response_time,
|
||||
status: match uptime.status {
|
||||
UptimeStatus::Up => String::from("Up"),
|
||||
UptimeStatus::Down => String::from("DOWN"),
|
||||
UptimeStatus::Maintenance => String::from("Undergoing Maintenance"),
|
||||
_ => String::from("Unknown"),
|
||||
},
|
||||
url: None,
|
||||
});
|
||||
}
|
||||
UptimeType::Service => {
|
||||
sv_uptime_infos.push(UptimeInfo {
|
||||
name: uptime.name,
|
||||
uptime: uptime.uptime,
|
||||
response_time: uptime.response_time,
|
||||
status: match uptime.status {
|
||||
UptimeStatus::Up => String::from("Up"),
|
||||
UptimeStatus::Down => String::from("DOWN"),
|
||||
UptimeStatus::Maintenance => String::from("Undergoing Maintenance"),
|
||||
_ => String::from("Unknown"),
|
||||
},
|
||||
url: Some(uptime.url),
|
||||
});
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
let service_template = StatusTemplate {
|
||||
dctr_uptime_infos: dc_uptime_infos,
|
||||
svc_uptime_infos: sv_uptime_infos,
|
||||
last_updated: lu_str,
|
||||
};
|
||||
Ok(ContentTemplate {
|
||||
page_title: Some("NWS | System Status".to_string()),
|
||||
page_desc: Some("Check the health of NWS datacenters and services hosted on NWS.".to_string()),
|
||||
content: service_template,
|
||||
})
|
||||
}
|
14
src/nws-api/calls.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {Incident, UptimeResponse} from "./types";
|
||||
|
||||
export async function getUptime(): Promise<UptimeResponse> {
|
||||
let response: Response = await fetch('https://api-nws.nickorlow.com/uptime');
|
||||
let uptime: UptimeResponse = await response.json();
|
||||
return uptime;
|
||||
}
|
||||
|
||||
export async function getIncidents(): Promise<Incident[]> {
|
||||
let response: Response = await fetch('https://api-nws.nickorlow.com/incidents');
|
||||
let incidents: Incident[] = await response.json();
|
||||
return incidents;
|
||||
}
|
||||
|
35
src/nws-api/types.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
export type UptimeRecord = {
|
||||
name: string,
|
||||
url: string,
|
||||
uptimeMonth: number,
|
||||
uptimeAllTime: number,
|
||||
isUp: boolean,
|
||||
undergoingMaintenance: boolean
|
||||
};
|
||||
|
||||
export type UptimeResponse = {
|
||||
datacenters: UptimeRecord[],
|
||||
services: UptimeRecord[],
|
||||
lastUpdated: string
|
||||
};
|
||||
|
||||
export type Blog = {
|
||||
id: number,
|
||||
title: string,
|
||||
author: string,
|
||||
content: string,
|
||||
imageUrl: string
|
||||
};
|
||||
|
||||
export type Incident = {
|
||||
id: number,
|
||||
severity: IncidentSeverity,
|
||||
title: string,
|
||||
description: string
|
||||
};
|
||||
|
||||
enum IncidentSeverity {
|
||||
LOW,
|
||||
MEDIUM,
|
||||
HIGH
|
||||
};
|
1
src/react-app-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="react-scripts" />
|
15
src/reportWebVitals.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
5
src/setupTests.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
BIN
src/static/images/NWS_Logo.png
Normal file
After Width: | Height: | Size: 28 KiB |
|
@ -1,250 +0,0 @@
|
|||
use anyhow::anyhow;
|
||||
use anyhow::Context;
|
||||
use chrono::{Datelike, NaiveDate};
|
||||
use log::*;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[macro_use]
|
||||
use dotenv::dotenv;
|
||||
use std::env;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum UptimeType {
|
||||
Provider,
|
||||
Service,
|
||||
Datacenter,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum UptimeStatus {
|
||||
Up,
|
||||
Down,
|
||||
Maintenance,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Uptime {
|
||||
pub name: String,
|
||||
pub uptime: String,
|
||||
pub response_time: String,
|
||||
pub status: UptimeStatus,
|
||||
pub uptime_type: UptimeType,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UptimeServiceState {
|
||||
uptimes: Vec<Uptime>,
|
||||
last_updated: SystemTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UptimeService {
|
||||
state: Arc<Mutex<UptimeServiceState>>,
|
||||
}
|
||||
|
||||
impl UptimeService {
|
||||
const UPDATE_SECONDS: u64 = 300;
|
||||
|
||||
pub fn new() -> Self {
|
||||
let init_state = Arc::new(Mutex::new(UptimeServiceState {
|
||||
uptimes: vec![],
|
||||
last_updated: UNIX_EPOCH,
|
||||
}));
|
||||
Self { state: init_state }
|
||||
}
|
||||
|
||||
pub fn start(&self) {
|
||||
info!("Starting UptimeService");
|
||||
let cloned_state = Arc::clone(&self.state);
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let clonedx_state = Arc::clone(&cloned_state);
|
||||
let res = Self::update_data(clonedx_state).await;
|
||||
match res {
|
||||
Err(err) => {
|
||||
error!("{}", err);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
sleep(tokio::time::Duration::from_secs(Self::UPDATE_SECONDS)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_data(&self) -> Vec<Uptime> {
|
||||
let state = self.state.lock().unwrap();
|
||||
let uptimes = state.uptimes.clone();
|
||||
return uptimes;
|
||||
}
|
||||
|
||||
pub fn get_last_updated(&self) -> SystemTime {
|
||||
let state = self.state.lock().unwrap();
|
||||
let lu = state.last_updated.clone();
|
||||
return lu;
|
||||
}
|
||||
|
||||
async fn update_data(arc_state: Arc<Mutex<UptimeServiceState>>) -> ::anyhow::Result<()> {
|
||||
debug!("Starting data update for UptimeService");
|
||||
|
||||
let mut request_vars = HashMap::new();
|
||||
let api_key = env::var("UPTIMEROBOT_API_KEY")?;
|
||||
request_vars.insert("api_key", api_key.as_str());
|
||||
request_vars.insert("all_time_uptime_ratio", "1");
|
||||
let now = SystemTime::now();
|
||||
//let thirty_days_ago = now - Duration::from_secs(30 * 24 * 3600);
|
||||
|
||||
let current_year = chrono::Utc::today().year();
|
||||
let january_1st = NaiveDate::from_ymd(current_year, 1, 1).and_hms(0, 0, 0);
|
||||
let duration =
|
||||
january_1st.signed_duration_since(NaiveDate::from_ymd(1970, 1, 1).and_hms(0, 0, 0));
|
||||
let year_start = UNIX_EPOCH + Duration::from_secs(duration.num_seconds() as u64);
|
||||
|
||||
//let ranges = &format!(
|
||||
// "{}_{}-{}_{}",
|
||||
// thirty_days_ago.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
||||
// now.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
||||
// year_start.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
||||
// now.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
||||
//);
|
||||
let ranges = &format!(
|
||||
"{}_{}",
|
||||
year_start.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
||||
now.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
||||
);
|
||||
request_vars.insert("custom_uptime_ranges", ranges);
|
||||
request_vars.insert("response_times_average", "1440");
|
||||
request_vars.insert("response_times", "1");
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.post("https://api.uptimerobot.com/v2/getMonitors")
|
||||
.form(&request_vars)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let resp = res.json::<serde_json::Value>().await?;
|
||||
|
||||
let monitors = resp
|
||||
.get("monitors")
|
||||
.context("Response did not have a monitors subobject")?
|
||||
.as_array()
|
||||
.context("Monitors subobject was not an array")?;
|
||||
|
||||
let mut state = match arc_state.lock() {
|
||||
Ok(val) => val,
|
||||
Err(_) => {
|
||||
return Err(anyhow!("Could not lock shared state"));
|
||||
}
|
||||
};
|
||||
state.uptimes.clear();
|
||||
for monitor in monitors {
|
||||
let monitor_fqn = monitor
|
||||
.get("friendly_name")
|
||||
.context("Monitor did not have property 'friendly_name'")?;
|
||||
|
||||
debug!("Monitor '{}' processing", monitor_fqn);
|
||||
|
||||
let split_str: Vec<&str> = monitor_fqn
|
||||
.as_str()
|
||||
.context("Expected 'friendly_name' to be a string")?
|
||||
.split(".")
|
||||
.collect();
|
||||
if split_str.len() != 2 {
|
||||
debug!("Monitor '{}' excluded due to bad format", monitor_fqn);
|
||||
continue;
|
||||
}
|
||||
|
||||
let monitor_nt = String::from(
|
||||
*split_str
|
||||
.get(0)
|
||||
.context("Expected name to have first part")?,
|
||||
);
|
||||
let monitor_name = String::from(
|
||||
*split_str
|
||||
.get(1)
|
||||
.context("Expected name to have second part")?,
|
||||
);
|
||||
let monitor_type = match monitor_nt.as_str() {
|
||||
"datacenter" => UptimeType::Datacenter,
|
||||
"service" => UptimeType::Service,
|
||||
"competitor" => UptimeType::Provider,
|
||||
_ => UptimeType::Unknown,
|
||||
};
|
||||
|
||||
if monitor_type == UptimeType::Unknown {
|
||||
debug!("Monitor '{}' excluded due to unknown type", monitor_fqn);
|
||||
continue;
|
||||
}
|
||||
|
||||
let monitor_status_num = monitor
|
||||
.get("status")
|
||||
.context("Expected monitor to have 'status' property")?
|
||||
.as_u64()
|
||||
.context("Expected 'status' property to be u64")?;
|
||||
|
||||
let monitor_status = match monitor_status_num {
|
||||
0 => UptimeStatus::Maintenance,
|
||||
1 | 8 | 9 => UptimeStatus::Down,
|
||||
2 => UptimeStatus::Up,
|
||||
_ => UptimeStatus::Unknown,
|
||||
};
|
||||
|
||||
if monitor_status == UptimeStatus::Unknown {
|
||||
debug!(
|
||||
"Monitor '{}' excluded due to unknown status (status was {})",
|
||||
monitor_fqn, monitor_status_num
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let monitor_rt_val = monitor
|
||||
.get("average_response_time")
|
||||
.context("Expected monitor to have property 'average_response_time'")?;
|
||||
|
||||
// Because UptimeRobot has the world's worst API ever
|
||||
// and decided that it's okay to return multiple datatypes
|
||||
// for one property based on how they're feeling
|
||||
let monitor_rt = match monitor_rt_val.as_str() {
|
||||
Some(string) => format!("{}ms", string),
|
||||
_ => format!("N/A"),
|
||||
};
|
||||
|
||||
let monitor_uptime = format!(
|
||||
"{}%",
|
||||
monitor
|
||||
.get("custom_uptime_ranges")
|
||||
.context("Expected monitor to have property 'custom_uptime_ranges'")?
|
||||
.as_str()
|
||||
.context("Expected 'custom_uptime_ranges' to be String")?
|
||||
);
|
||||
|
||||
let monitor_url = String::from(
|
||||
monitor
|
||||
.get("url")
|
||||
.context("Expected monitor to have property 'url'")?
|
||||
.as_str()
|
||||
.context("Expected 'url' to be String")?,
|
||||
);
|
||||
|
||||
state.uptimes.push(Uptime {
|
||||
name: monitor_name,
|
||||
uptime: monitor_uptime,
|
||||
response_time: monitor_rt,
|
||||
status: monitor_status,
|
||||
uptime_type: monitor_type,
|
||||
url: monitor_url,
|
||||
});
|
||||
}
|
||||
|
||||
state.last_updated = SystemTime::now();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
<h1>Blog</h1>
|
||||
|
||||
{% for blog in blogs %}
|
||||
<div style="margin-top: 10px; margin-bottom: 10px;">
|
||||
<h3 style="margin-bottom: 0px;">
|
||||
<a
|
||||
{% if blog.url.contains("https://") %}
|
||||
href="{{ blog.url }}"
|
||||
{% else %}
|
||||
href="/blogs/{{ blog.url }}"
|
||||
{% endif %}
|
||||
>
|
||||
[ {{ blog.title }} ]
|
||||
</a>
|
||||
</h3>
|
||||
<p style="margin-top: 0px;"><i>{{ blog.date }}</i></p>
|
||||
</div>
|
||||
{% endfor %}
|
|
@ -1,89 +0,0 @@
|
|||
<h1>NWS Incident Postmortem 11/08/2023</h1>
|
||||
|
||||
<p>
|
||||
On November 8th, 2023 at approximately 09:47 UTC, NWS suffered
|
||||
a complete outage. This outage resulted in the downtime of all
|
||||
services hosted on NWS and the downtime of the NWS Management
|
||||
Engine and the NWS dashboard.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The incident lasted 38 minutes after which it was automatically
|
||||
resolved and all services were restored. This is NWS' first
|
||||
outage event of 2023.
|
||||
</p>
|
||||
|
||||
<h2>Cause</h2>
|
||||
<p>
|
||||
NWS utilizes several tactics to ensure uptime. A component of
|
||||
this is load balancing and failover. This service is currently
|
||||
provided by Cloudflare at the DNS level. Cloudflare sends
|
||||
health check requests to NWS servers at specified intervals. If
|
||||
it detects that one of the servers is down, it will remove the
|
||||
A record from entry.nws.nickorlow.com for that server (this domain
|
||||
is where all services on NWS direct their traffic via a
|
||||
CNAME).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
At around 09:47 UTC, Cloudflare detected that our servers in
|
||||
Texas (Austin and Hill Country) were down. It did not detect an
|
||||
error, but rather an HTTP timeout. This is an indication that the
|
||||
server may have lost network connectivity. When Cloudflare detected that the
|
||||
servers were down, it removed their A records from the
|
||||
entry.nws.nickorlow.com domain. Since NWS Pennsylvania servers
|
||||
have been undergoing maintenance since August 2023, this left no
|
||||
servers able to serve requests routed to entry.nws.nickorlow.com,
|
||||
resulting in the outage.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
NWS utilizes UptimeRobot for monitoring the uptime statistics of
|
||||
services on NWS and NWS servers. This is the source of the
|
||||
statistics shown on the NWS status page.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
UptimeRobot did not detect either of the Texas NWS servers as being
|
||||
offline for the duration of the outage. This is odd, as UptimeRobot
|
||||
and Cloudflare did not agree on the status of NWS servers. Logs
|
||||
on NWS servers showed that requests from UptimeRobot were being
|
||||
served while no requests from Cloudflare were shown in the logs.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
No firewall rules existed that could have blocked the healthcheck traffic from Cloudflare
|
||||
for either of the NWS servers. There was no other configuration
|
||||
found that would have blocked these requests. As these servers
|
||||
are on different networks inside different buildings in different
|
||||
parts of Texas, their networking equipment is entirely separate.
|
||||
This rules out any failure of networking equipment owned
|
||||
by NWS. This leads us to believe that the issue may have been
|
||||
caused due to an internet traffic anomaly, although we are currently
|
||||
unable to confirm that this is the cause of the issue.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This is being actively investigated to find a more concrete root
|
||||
cause. This postmortem will be updated if any new information is
|
||||
found.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
A similar event occurred on November 12th, 2023 lasting for 2 seconds.
|
||||
</p>
|
||||
|
||||
<h2>Fix</h2>
|
||||
<p>
|
||||
The common factor between both of these servers is that they both use
|
||||
Spectrum for their ISP and that they are located near Austin, Texas.
|
||||
The Pennsylvania server maintenance will be expedited so that we have
|
||||
servers online that operate with no commonalities.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
NWS will also investigate other methods of failover and load
|
||||
balancing.
|
||||
</p>
|
||||
|
||||
<p>Last updated on November 16th, 2023</p>
|
|
@ -1,51 +0,0 @@
|
|||
<h1>NWS Incident Postmortem 11/28/2024 - Present</h1>
|
||||
|
||||
<p>
|
||||
On November 28th, 2024 at approximately 07:37 UTC, NWS suffered
|
||||
a complete outage. This outage resulted in the downtime of all
|
||||
services hosted on NWS and the downtime of the NWS Management
|
||||
Engine and the NWS dashboard.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The incident lasted 10 days and 15 hours after which it was manually
|
||||
resolved and all services were restored. This was NWS' first
|
||||
outage event of 2024.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Since then, similar outages have occurred.
|
||||
</p>
|
||||
|
||||
<h2>Cause</h2>
|
||||
<p>
|
||||
NWS utilizes several tactics to ensure uptime. A component of
|
||||
this is load balancing and failover. Due to logistical issues,
|
||||
only one NWS point of presence has been operating since early
|
||||
November 2024. This means that any issue with the remaining
|
||||
datacenter will result in a total outage. More points of presence
|
||||
are expected to be brought online in August 2024. Similar incidents are
|
||||
expected until then.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This outage lasted 10 days due to the fact that I was busy with
|
||||
school. I'm not super concerned about maintaining high uptime with
|
||||
only one server, and I'm pretty happy with NWS since we hit 100% uptime
|
||||
for a >365 day period.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The cause of the outage was that the Xfinity ( yeah :( ) router that
|
||||
NWS uses in the Pottsville location encountered an issue which caused
|
||||
it to automatically drop all port forwards. To combat this issue, a new
|
||||
Ubiquiti EdgeMax router is scheduled to be installed in December 2024.
|
||||
</p>
|
||||
|
||||
|
||||
<h2>Fix</h2>
|
||||
<p>
|
||||
The port forwards were restored and the router is scheduled to be replaced.
|
||||
</p>
|
||||
|
||||
<p>Last updated on December 28th, 2024</p>
|
|
@ -1,9 +0,0 @@
|
|||
<h1>Goodbye, NWS</h1>
|
||||
|
||||
<p>
|
||||
<b>
|
||||
Nick Web Services (NWS) is now Nick Web Services (NWS).
|
||||
</b>
|
||||
</p>
|
||||
|
||||
<p>That is all</p>
|
|
@ -1,2 +0,0 @@
|
|||
<h1>Under Construction</h1>
|
||||
<p>The new dashboard isn't ready yet! Nobody but me used it anyways!</p>
|
|
@ -1,31 +0,0 @@
|
|||
{%- import "uptime_table.html" as scope -%}
|
||||
|
||||
<div>
|
||||
<h1 style="margin-bottom: 0px;margin-top: 0px;">Nick Web Services</h1>
|
||||
<p style="margin-top: 0px;">Pottsville, PA - Philadelphia, PA - Austin, TX</p>
|
||||
|
||||
|
||||
<p>
|
||||
Nick Web Services is a hosting service based out of the Commonwealth of Pennsylvania
|
||||
and the State of Texas.
|
||||
We are committed to achieving maximum uptime with better performance and a lower
|
||||
cost than any of the major cloud services.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We operate four datacenters located across three cities in two states. This infrastructure setup ensures redundancy and failover capabilities, minimizing downtime risks. Additionally, the geographical distribution enhances speed and accessibility, reducing latency for users across different regions.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This has led to us maintaining four nines availability (99.9931% ; 38 minutes of downtime
|
||||
all year) for 2023 and <b>100% uptime for the period from 11/8/2023 to 11/28/2024 (over a year!). This was the original goal of NWS.</b>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Currently, NWS is only able to operate with one point of presence and as such, will
|
||||
have reduced uptime. This is expected to be resolved around August 2024.
|
||||
</p>
|
||||
|
||||
<h2>Compare us to our competitors!</h2>
|
||||
{% call scope::uptime_table(uptime_infos) %}
|
||||
</div>
|
|
@ -1,54 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
{% if let Some(title) = page_title %}
|
||||
<title>{{ title }}</title>
|
||||
{% else %}
|
||||
<title>Nick Web Services</title>
|
||||
{% endif %}
|
||||
|
||||
{% if let Some(desc) = page_desc %}
|
||||
<meta name="{{ desc }}" />
|
||||
{% else %}
|
||||
<meta name="Nick Web Services" />
|
||||
{% endif %}
|
||||
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div>
|
||||
<a class="nav-link" href="/">[ Home ]</a>
|
||||
<a class="nav-link" href="/system_status">[ System Status ]</a>
|
||||
<a class="nav-link" href="/blog">[ Blog ]</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="nav-link" href="/dashboard">[ Dashboard ]</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<hr/>
|
||||
|
||||
{{ content|safe }}
|
||||
|
||||
<footer>
|
||||
<hr />
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div>
|
||||
<p style="margin-bottom: 0px; margin-top:0px;"><b>Nick Web Services</b></p>
|
||||
<p style="margin-bottom: 0px;margin-top: 0px;">
|
||||
<small>Copyright © <a href="https://nickorlow.com">Nicholas Orlowsky</a> 2024</small>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<img class="flag-img" src="/assets/flag-images/us.png" title="The United States of America"/>
|
||||
<img class="flag-img" src="/assets/flag-images/us-pa.png" title="The Commonwealth of Pennsylvania"/>
|
||||
<img class="flag-img" src="/assets/flag-images/us-tx.png" title="The State of Texas"/>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
|
@ -1,26 +0,0 @@
|
|||
{%- import "uptime_table.html" as scope -%}
|
||||
|
||||
<h1>System Status</h1>
|
||||
|
||||
<h2>Datacenter Status</h2>
|
||||
<p>
|
||||
The status of each of Nick Web Services's 4
|
||||
datacenters.
|
||||
</p>
|
||||
|
||||
{% call scope::uptime_table(dctr_uptime_infos) %}
|
||||
|
||||
<p>
|
||||
Notice: Due to leasing issues, the Philadelphia datacenter will be offline until
|
||||
at least May or August 2025 or it may be discontinued as an NWS location.
|
||||
</p>
|
||||
|
||||
<h2>Service Status</h2>
|
||||
<p>
|
||||
The status of services people host on Nick Web Services.
|
||||
Note that the uptime and performance of services hosted on
|
||||
Nick Web Services may be affected by factors not controlled by us such as
|
||||
bad optimization or buggy software.
|
||||
</p>
|
||||
|
||||
{% call scope::uptime_table(svc_uptime_infos) %}
|
|
@ -1,35 +0,0 @@
|
|||
{% macro uptime_table(uptime_infos) %}
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Uptime YTD</th>
|
||||
<th>Response Time 24h</th>
|
||||
<th>Current Status</th>
|
||||
</tr>
|
||||
{% for uptime_info in uptime_infos %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if let Some(click_url) = uptime_info.url %}
|
||||
<a href="{{click_url}}">
|
||||
{% endif %}
|
||||
|
||||
{{uptime_info.name}}
|
||||
|
||||
{% if let Some(click_url) = uptime_info.url %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{uptime_info.uptime}}</td>
|
||||
<td>{{uptime_info.response_time}}</td>
|
||||
<td
|
||||
{% if uptime_info.status != "Up" %}
|
||||
style="color: red;"
|
||||
{% endif %}
|
||||
>
|
||||
{{uptime_info.status}}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<p style="margin-top: 0px;"><i>Data current as of {{last_updated}}</i></p>
|
||||
{% endmacro %}
|
26
tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|