Compare commits

..

No commits in common. "done" and "main" have entirely different histories.
done ... main

71 changed files with 3073 additions and 31732 deletions

View file

@ -1,13 +1,9 @@
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
@ -23,6 +19,8 @@ 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
@ -36,11 +34,28 @@ 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: ${{ steps.meta.outputs.tags }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vnum.outputs.version_tag }}
${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

25
.gitignore vendored
View file

@ -1,23 +1,2 @@
# 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*
/target
.env

View file

@ -1,8 +0,0 @@
<?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>

View file

@ -1,12 +0,0 @@
<?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>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View file

@ -1,85 +0,0 @@
<?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 beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/index.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="HTTP Request" />
<option value="TypeScript File" />
<option value="TypeScript JSX File" />
<option value="CSS 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="ts.external.directory.path" value="$PROJECT_DIR$/node_modules/typescript/lib" />
</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src/components" />
<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="3699000" />
<workItem from="1668047509596" duration="7310000" />
<workItem from="1673378530233" duration="327000" />
<workItem from="1673538703809" duration="6147000" />
<workItem from="1674698447115" duration="249000" />
</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 Normal file

File diff suppressed because it is too large Load diff

24
Cargo.toml Normal file
View file

@ -0,0 +1,24 @@
[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"

View file

@ -1,18 +1,14 @@
# 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
FROM rust:1.74.1 as build
RUN npm install react-scripts@3.4.1 -g --silent
COPY . ./
RUN npm run build
ENV PKG_CONFIG_ALLOW_CROSS=1
WORKDIR .
COPY . .
RUN cargo install --path .
ENV RUST_LOG=info
ENV EXPOSE_PORT=80
# 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
CMD ["nginx", "-g", "daemon off;"]
ENTRYPOINT ["website"]

View file

@ -1,9 +1 @@
<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
# Nick Web Services Web(site/API)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

BIN
assets/flag-images/us.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

45
assets/style.css Normal file
View file

@ -0,0 +1,45 @@
* {
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 Normal file
View file

@ -0,0 +1,10 @@
.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

View file

@ -1,30 +0,0 @@
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;
}
}

29692
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,60 +0,0 @@
{
"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": "^18.2.0",
"react-bootstrap": "^2.4.0",
"react-copy-code": "^2.1.2",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.3",
"react-modal": "^3.16.1",
"react-router-dom": "^6.4.2",
"react-scripts": "5.0.0",
"react-tooltip": "^4.4.3",
"strip-markdown": "^5.0.0",
"typescript": "^4.5.5",
"urijs": "^1.19.11",
"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"
]
},
"devDependencies": {
"@types/react-modal": "^3.13.1",
"@types/urijs": "^1.19.19"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -1,42 +0,0 @@
<!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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -1,25 +0,0 @@
{
"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"
}

View file

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View file

@ -1,76 +0,0 @@
.App {
display: flex;
align-items: center;
flex-direction: column;
}
input {
border-radius: 10px;
border-color: #000;
border-width: 1px;
border-style: solid;
margin-bottom: 1px;
display: block;
padding: 3px;
}
button {
border-radius: 10px !important;
border-width: 0px;
background-color: cornflowerblue;
color: white;
margin-top: 10px;
margin-bottom: 10px;
padding: 3px
}
p {
margin: 0 !important;
}
.error-banner {
background-color: #f08080;
border-radius: 10px;
padding: 3px;
height: max-content;
}
.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;
}
@media only screen
and (max-width: 992px) {
.nws-card-md {
border-radius: 20px;
background-color: #eee;
padding-bottom: 2rem;
}
}
.nws-card {
border-radius: 20px;
background-color: #eee;
padding: .75rem;
}

View file

@ -1,9 +0,0 @@
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();
});

View file

@ -1,16 +0,0 @@
import React, {useEffect, useState} from 'react';
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import StatusPage from "./components/StatusPage";
import Footer from "./components/Footer";
import { BrowserRouter, Route, Outlet, Link } from "react-router-dom";
function App() {
return (
<div/>
);
}
export default App;

View file

@ -1,11 +0,0 @@
.blog-card {
transition: 1s;
width: 80%;
background-color: #eee;
border-radius: 20px;
overflow: clip;
}
.blog-card:hover {
transform: translateY(-10px);
}

View file

@ -1,41 +0,0 @@
import {useEffect, useState} from "react";
import {Blog, Incident, UptimeResponse} from "../nws-api/types";
import {getBlogs, getIncidents, getUptime} from "../nws-api/calls";
import "./Blog.css";
import ReactMarkdown from 'react-markdown';
import strip from 'strip-markdown';
export default function Blogs(){
const [blogs, setBlogs] = useState<Blog[]>([]);
const fetchBlogs = async () => {
let resp: Blog[] = await getBlogs();
setBlogs(resp);
}
useEffect(() => {
fetchBlogs();
}, []);
return(
<div>
<h1>Blogs</h1>
<div className={"d-flex justify-content-center"}>
{blogs.map((e)=>{
return(
<div className={"blog-card row"} onClick={()=>{window.location.href=`/blog?id=${e.id}`}}>
<img src={e.imageUrl} className={"col-md-4 m-0 p-0"}/>
<div className={"p-2 col-md-8"}>
<h2>{e.title}</h2>
<p>By: {e.author}</p>
<p style={{maxHeight: 100, overflow: "clip"}}><ReactMarkdown remarkPlugins={[strip]}>{e.content}</ReactMarkdown>...</p>
<p><b>Click to read more</b></p>
</div>
</div>
);
})}
</div>
</div>
);
}

View file

@ -1,32 +0,0 @@
.pill {
padding: 1px;
transition: .5s;
text-align: center;
}
.pill-selected{
font-weight: bold;
border-radius: 20px;
background-color: #ffffff;
}
.pill-container {
padding: 2px;
border-radius: 20px;
background-color: #aaaa;
}
button {
padding: 5px;
}
.help-text {
font-style: italic;
margin: 0;
font-size: .9em;
}
.label-text{
margin: 1em 0 0;
}

View file

@ -1,194 +0,0 @@
import {useState} from "react";
import URI from "urijs";
import {Namespace} from "../nws-api/types";
import {useNWSAccount, useNWSAuthKey} from "../nws-api/hooks";
import {useSearchParams} from "react-router-dom";
import './CreateCruisePage.css';
export default function CreateCruisePage() {
const [page, setPage] = useState('info');
const [strat, setStrat] = useState<'raw-html' | 'react-js'>('raw-html');
const [owner, setOwner] = useState('');
const [repo, setRepo] = useState('');
const [name, setName] = useState('');
const [gitUriInput, setGUI] = useState('');
const [hostUriInput, setHUI] = useState('');
const authKey = useNWSAuthKey();
const acct = useNWSAccount();
const [search, useSearch] = useSearchParams();
function deploy() {
fetch("https://api-nws.nickorlow.com/" + acct!.id + "/service",
{
method: 'POST',
headers: {
"Authorization": authKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"serviceName": name,
"containerUrl": `ghcr.io/${owner}/${repo}`,
"namespaceId": search.get("namespaceId"),
"serviceUrl": hostUriInput,
})
}).then((response)=> {
if(response.status === 200) {
}
}).catch((ex) =>{
alert(ex)
});
}
return (
<div className={"App"}>
<h1 className={"mb-5 mt-3 fw-bolder"}>Create Container Deployment</h1>
<div className={"pill-container row justify-content-evenly mb-5"} style={{width: "80%"}}>
<p className={"pill col-md-2 " + (page === 'info' ? "pill-selected" : "")}>About</p>
<p className={"pill col-md-2 " + (page === 'framework-hostname' ? "pill-selected" : "")}>Deployment Info</p>
<p className={"pill col-md-2 " + (page === 'scriptgen' ? "pill-selected" : "")}>Repo Setup</p>
<p className={"pill col-md-2 " + (page === 'dns' ? "pill-selected" : "")}>DNS Configuration</p>
</div>
<div style={{width: "75vw"}}>
{
page === 'info' &&
<div>
<h3>Some information before we get started:</h3>
<ul>
<li>NWS is free to use</li>
<li>Currently, your DNS provider must support DNS flattening if you intend to point your root domain (e.g. nickorlow.com) to NWS. Subdomains should work fine though. (Cloudflare, Route 53, and Pagely). (Moving to Cloudflare is pretty easy)</li>
<li>Through the Web UI, you may only add one domain name. If you need to add more, <a href={"mailto:nws-support@nickorlow.com"}>contact me</a></li>
<li>NWS does not guarantee any uptime</li>
<li>NWS is run by a college student with little free time, support may reflect this</li>
<li>This platform is very early in development. It may require you to have some technical
knowledge.
</li>
<li>NWS may cease operations in the event of a widespread viral infection transmitted via
bites or contact with bodily fluids that causes human corpses to reanimate and seek to
consume living human flesh, blood, brain or nerve tissue and is likely to result in the
fall of organized civilization.
</li>
</ul>
<button className={"float-end"} onClick={()=>setPage('framework-hostname')}>I Understand, Continue</button>
</div>
}
{
page === 'framework-hostname' &&
<div>
<h5 className={"label-text"}>What is this deployment's name?</h5>
<p className={"help-text"}>May only be lowercase letters and dashes, max 20 chars</p>
<input value={name} onChange={(e)=>{setName(e.currentTarget.value)}}/>
<h5 className={"label-text"}>How did you create your website?</h5>
<p className={"help-text"}>Don't see your technology/framework? Email me: <a href={"mailto:nws-support@nickorlow.com"}>nws-support@nickorlow.com</a></p>
<select value={strat}>
<option id={"raw-html"} onClick={()=>setStrat('raw-html')}>Raw HTML</option>
<option id={"react-js"} onClick={()=>setStrat('react-js')}>React JS</option>
</select>
<h5 className={"label-text"}>What is the url of the GitHub repo where your code is hosted?</h5>
<p className={"help-text"}>Other git hosting providers are not currently supported through the Web UI</p>
<p className={"help-text"}>The repo must be public to create it through the Web UI</p>
<input placeholder={"https://github.com/nickorlow/personal-site"} value={gitUriInput} onInput={(e)=>{setGUI(e.currentTarget.value)}}/>
<h5 className={"label-text"}>What domain name will you use with your website?</h5>
<input placeholder={"nws.nickorlow.com"} value={hostUriInput} onChange={(e)=>{setHUI(e.currentTarget.value)}}/>
<button onClick={()=>{setPage('info')}}>Back</button>
<button className={"float-end"} onClick={()=>{
try {
let git_url = new URL(gitUriInput);
if (git_url.host !== 'github.com') {
alert('Only github is supported!')
return;
} else {
console.log(git_url.pathname.split('/'))
setOwner(git_url.pathname.split('/')[1])
setRepo(git_url.pathname.split('/')[2])
}
} catch (e) {
alert('invalid github url')
return;
}
try {
let url = new URL("https://"+hostUriInput);
} catch (e) {
alert('invalid host url')
return;
}
if(!/^[a-z-]+$/.test(name) || name.length > 20 || name.length == 0) {
alert('may only be lowercase and dashes and under 20 chars')
return;
}
setPage('scriptgen')
}}>Continue</button>
</div>
}
{
page === 'scriptgen' &&
<div>
<h4>Copy & Paste the below into your terminal to add NWS deployment scripts to your webapp</h4>
<code lang={"shell"} style={{backgroundColor: "black", padding: 5, borderRadius: 10}}>
curl -s https://raw.githubusercontent.com/nickorlow/nws-ghactions-templates/main/add-nws.sh | bash -s {strat} {owner} {repo}
</code>
<br/><span>Ensure the script finishes running before continuing</span>
<br/>
<button onClick={()=>setPage('framework-hostname')}>Back</button>
<button className={"float-end"} onClick={()=>{
deploy();
setPage('dns');
}}>Continue</button>
</div>
}
{
page === 'dns' &&
<div>
<h4>Add the following DNS entry to {new URI("https://"+hostUriInput).hostname()}</h4>
{
new URI("https://"+hostUriInput).subdomain().length == 0 &&
<div>
<p>If your DNS provider is:</p>
<ul>
<li>Cloudflare</li>
<li>Route 53</li>
<li>Pagely</li>
</ul>
<p>Type: CNAME</p>
<p>Name: @ ({hostUriInput})</p>
<p>Value: entry.nws.nickorlow.com</p>
</div>
}
{
new URI("https://"+hostUriInput).subdomain().length > 0 &&
<div>
<p>Type: CNAME</p>
<p>Name: {new URI(hostUriInput).subdomain()} ({new URI(hostUriInput).hostname()})</p>
<p>Value: entry.nws.nickorlow.com</p>
</div>
}
<br/>
<button onClick={()=>setPage('done')}>Finish Setup</button>
</div>
}
{
page === 'done' &&
<div>
<h3>Welcome to NWS</h3> <br/>
<button onClick={()=>{window.location.href="/dashboard"}}>Go to Dashboard</button> <br/>
<button onClick={()=>{window.location.href=hostUriInput}}>See my Site</button>
</div>
}
</div>
</div>
);
}

View file

@ -1,53 +0,0 @@
import {Account, Namespace, Service} from "../nws-api/types";
import {
useGetAccountNamespaces,
useGetAccountServices,
useGetServicesInNamespace,
useLoggedInRedirect,
useNWSAccount
} from "../nws-api/hooks";
import {useState} from "react";
export default function DashboardPage() {
useLoggedInRedirect();
let account: Account | undefined = useNWSAccount();
let {setNs, services, ns} = useGetServicesInNamespace();
let namespaces: Namespace[] = useGetAccountNamespaces();
return(
<div style={{minHeight: "100vh", padding: "50px"}}>
<div className={"row"}>
<h1 className={"col-md-10 col-12"}>Welcome to NWS, {account?.name}!</h1>
<select className={"col-12 col-md-2"} defaultValue={"Select Namespace..."}>
<option value="" disabled selected>Select Namespace...</option>
{
namespaces.map((e)=>{
return <option onClick={(a)=>{setNs(e)}}>{e.name}</option>
})
}
<option value="" disabled>---</option>
<option value="create-ns">Create Namespace</option>
</select>
</div>
<hr/>
<div className={"d-flex justify-content-between"}>
<h2>Container Deployment Services</h2>
<button onClick={(e) => {window.location.href = "/cruise/new?namespaceId="+ns!.id}}>Create Cruise Service</button>
</div>
<div className={"row"}>
{services.map((e)=>{
return (
<div className={"col-4"} style={{ padding: 5}}>
<div style={{backgroundColor: "#eee", borderRadius: 20, padding: 5}}>
<h3>{e.serviceName}</h3>
<p><b>Application Id</b></p>
<p>{e.serviceId}</p>
</div>
</div>);
})}
</div>
</div>
);
}

View file

@ -1,10 +0,0 @@
import React from "react";
export default function Footer() {
return (
<footer className={"mt-2 p-3"} style={{backgroundColor: "#eee"}}>
<p>NWS is owned and operated by <a href={"http://nickorlow.com"}>Nicholas Orlowsky</a>.</p>
<p>Copyright © Nicholas Orlowsky {new Date().getFullYear()}</p>
</footer>
);
}

View file

@ -1,71 +0,0 @@
import NWSLogo from "../static/images/NWS_Logo_Transparent.png";
import UptimeCard from "./UptimeCard";
import IncidentCard from "./IncidentCard";
import Footer from "./Footer";
import React, {useEffect, useState} from "react";
import {Incident, UptimeResponse} from "../nws-api/types";
import {getIncidents, getUptime} from "../nws-api/calls";
import "../App.css";
import UptimeComparisonCard from "./UptimeComparisonCard";
import UptimeLabelCard from "./UptimeLabelCard";
export default function HomePage() {
const [uptime, setUptime] = useState<UptimeResponse>({datacenters: [], services: [], competitors: [], 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();
}, []);
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 className={"w-100 mt-2 flex justify-content-center align-content-center text-center"}>
<h3><i>Better uptime than GitHub Pages!</i></h3>
<h4><a href={"https://youtu.be/WHdXWMFHuqA"} target="_blank" rel="noopener noreferrer">Watch the NWS Deployment Demo</a></h4>
</div>
<div style={{width: '75vw'}}>
<hr/>
</div>
<div className={"text-left row"} style={{width: '75vw'}}>
<h2>Compare us to our competitors</h2>
<p>Last updated at {new Date(uptime.lastUpdated).toLocaleString()}</p>
<div className={"col-12 row w-100 m-0 align-content-center d-flex justify-content-center pt-2"}>
<UptimeLabelCard/>
{uptime.competitors.sort((a,b)=>{return b.uptimeMonth === a.uptimeMonth ? (a.name === "NWS" ? -1000 : b.name.localeCompare(a.name)) : b.uptimeMonth - a.uptimeMonth}).map((e) => {
return (
<UptimeComparisonCard uptime={e} isService={false}/>
);
})}
</div>
</div>
</div>
);
}

View file

@ -1,30 +0,0 @@
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>
);
}

View file

@ -1,32 +0,0 @@
.login-box {
border-style: solid;
border-width: 1px;
border-color: #aaa;
border-radius: 10px;
align-self: center;
justify-self: center;
width: 500px;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: center;
}
.login-label {
margin: 0;
}
.login-button {
border-radius: 10px;
border-width: 0px;
background-color: cornflowerblue;
color: white;
margin-top: 10px;
margin-bottom: 10px;
padding: 3px
}

View file

@ -1,64 +0,0 @@
import "./Login.css";
import {useEffect, useState} from "react";
import {Account, ApiError, SessionKey} from "../nws-api/types";
import {useNonLoggedInRedirect} from "../nws-api/hooks";
export default function LoginPage() {
useNonLoggedInRedirect();
const [errorMessage, setErrorMessage] = useState<string>("");
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
function loginUser() {
if(email == "" || password == "") {
setErrorMessage("Please enter an email and password");
return;
}
let acc: Account = {
email: email,
password: password
};
fetch("https://api-nws.nickorlow.com/Account/session",{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(acc)
}).then((result) => {
if(result.status == 200) {
result.json().then((o: SessionKey)=>{
localStorage.setItem("session_key", JSON.stringify(o));
window.location.href = '/dashboard';
});
} else {
setErrorMessage("Server Error (This is NWS' fault)");
}
}).catch((e) =>{
setErrorMessage("Server Error (This is NWS' fault)");
});
}
return(
<div style={{minHeight: "100vh", display: "grid", width: "100%"}}>
<div className={"login-box"}>
<h3>Login to NWS Dashboard</h3>
{ errorMessage != "" &&
<div className={"error-banner"}>
<p style={{color: "black"}}>{errorMessage}</p>
</div>
}
<p className={"login-label"}>E-Mail Address</p>
<input onChange={(e)=>{setEmail(e.target.value)}} className={"login-input"}/>
<p className={"login-label"}>Password</p>
<input onChange={(e)=>{setPassword(e.target.value)}}className={"login-input"} type={"password"}/>
<button className={"login-button"} onClick={loginUser}>Login</button>
<p>No account? <a href={"/register"}>Register Here!</a></p>
</div>
</div>
);
}

View file

@ -1,10 +0,0 @@
export default function NotFoundPage() {
return(
<div style={{width: "100vw", height: "100vh", display: "flex", justifyContent: "center", alignContent: "center", alignItems: "center"}}>
<div>
<h1>Not Found :(</h1>
<a href={"/"}>Home</a>
</div>
</div>
);
}

View file

@ -1,32 +0,0 @@
.reg-box {
border-style: solid;
border-width: 1px;
border-color: #aaa;
border-radius: 10px;
align-self: center;
justify-self: center;
width: 500px;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: center;
}
.reg-label {
margin: 0;
}
.reg-button {
border-radius: 10px;
border-width: 0px;
background-color: cornflowerblue;
color: white;
margin-top: 10px;
margin-bottom: 10px;
padding: 3px
}

View file

@ -1,105 +0,0 @@
import "./RegisterPage.css";
import {useState} from "react";
import {Account, ApiError} from "../nws-api/types";
import {useNonLoggedInRedirect} from "../nws-api/hooks";
export default function RegisterPage() {
const [errorMessage, setErrorMessage] = useState<String>("");
const [name, setName] = useState<string>("");
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [cpassword, setCpassword] = useState<string>("");
const [inviteCode, setInviteCode] = useState<string>("");
const [didRegister, setDidRegister] = useState<Boolean>(false);
async function createAccount() {
if(name == "" || email == "" || password == "" || cpassword == "" || inviteCode == "") {
setErrorMessage("You must fill out all information before registering.")
return;
}
if(cpassword != password) {
setErrorMessage("Passwords don't match!")
return;
}
if(!email
.toLowerCase()
.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
)) {
setErrorMessage("You have entered an invalid E-Mail address.")
return;
}
let newAcc: Account = {
email: email,
name: name,
password: password
};
fetch("https://api-nws.nickorlow.com/Account",{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Invite-Code': inviteCode
},
body: JSON.stringify(newAcc)
}).then((result) => {
if(result.status == 200) {
setDidRegister(true);
} else {
result.json().then((o: ApiError) => {
setErrorMessage(o.ErrorMessage);
});
}
}).catch((e) =>{
setErrorMessage("Server Error (This is NWS' fault)")
});
}
useNonLoggedInRedirect();
return(
<div style={{minHeight: "100vh", display: "grid", width: "100%"}}>
<div className={"reg-box"} style={{display: didRegister ? "none" : "flex"}}>
<h3>Create an NWS Account</h3>
{ errorMessage != "" &&
<div className={"error-banner"}>
<p style={{color: "black"}}>{errorMessage}</p>
</div>
}
<p className={"reg-label"}>Name</p>
<input onChange={(e)=>setName(e.target.value)} className={"reg-input"}/>
<p className={"reg-label"}>E-Mail Address</p>
<input onChange={(e)=>setEmail(e.target.value)} className={"reg-input"}/>
<p className={"reg-label"}>Password</p>
<input onChange={(e)=>setPassword(e.target.value)} className={"reg-input"} type={"password"}/>
<p className={"reg-label"}>Confirm</p>
<input onChange={(e)=>setCpassword(e.target.value)} className={"reg-input"} type={"password"}/>
<div style={{width: "100%", marginTop: 10, marginBottom: 10, padding: 10, backgroundColor: "#eee", borderRadius: 10}}>
<p className={"reg-label"}>Invite Code</p>
<input onChange={(e)=>setInviteCode(e.target.value)} style={{width: "100%"}}/>
<small>Currently, NWS is invite only. Email me to get an invite code.</small>
</div>
<button onClick={createAccount} className={"reg-button"}>Create Account</button>
<p>Already have an account? <a href={"/login"}>Login Here!</a></p>
</div>
<div className={"reg-box"} style={{display: didRegister ? "flex" : "none"}}>
<h3>Verify your E-Mail address.</h3>
<p>Please verify your E-Mail by clicking the link we sent to you at: <b>{email}</b></p>
</div>
</div>
);
}

View file

@ -1,73 +0,0 @@
import NWSLogo from "../static/images/NWS_Logo.png";
import UptimeCard from "./UptimeCard";
import IncidentCard from "./IncidentCard";
import Footer from "./Footer";
import React, {useEffect, useState} from "react";
import {Incident, UptimeResponse} from "../nws-api/types";
import {getIncidents, getUptime} from "../nws-api/calls";
import "../App.css";
export default function StatusPage() {
const [uptime, setUptime] = useState<UptimeResponse>({datacenters: [], services: [], competitors: [], 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();
}, []);
return(
<div className="App" style={{padding: 20}}>
<div className={"text-left row"} style={{width: '75vw'}}>
<h1>NWS System Status</h1>
<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} isService={true}/>
);
})}
</div>
<div className={"col-md-6 col-12"}>
<h3>Datacenter Status</h3>
{uptime.datacenters.map((e) => {
return (
<UptimeCard uptime={e} isService={false}/>
);
})}
</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>
</div>
);
}

View file

@ -1,46 +0,0 @@
.uptime-lnk {
font-weight: bolder;
color: #2f2f2f;
font-size: 1.1rem;
text-decoration: none;
transition: .5s;
cursor: pointer;
}
.uptime-lnk:hover {
color: #e08b0d;
}
.uptime-modal {
border-radius: 20px;
border-color: transparent;
background-color: #eee;
padding: 20px;
position: absolute;
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
width: 500px;
max-width: 100vw;
}
.stat-perfect {
font-weight: bold;
color: #069D06
}
.stat-middle {
color: #877E1C
}
.stat-bad {
color: #921111
}
hr {
height: 1.1px;
}

View file

@ -1,52 +0,0 @@
import {UptimeRecord} from "../nws-api/types";
import React, {useState} from "react";
import '../App.css';
import "./UptimeCard.css"
import Modal from "react-modal";
export default function UptimeCard(props: {uptime: UptimeRecord, isService: boolean}) {
const [isModalOpen, setModalOpen] = useState(false);
return(
<div className={"nws-card row mb-2 m-0"} style={{maxWidth: '100%'}}>
<h4 className={"col-md-9 col-12 uptime-lnk"} onClick={()=>setModalOpen(true)}>{props.uptime.name}</h4>
<div className={`col-md-3 col-12 d-flex d-md-none justify-content-start`}>
<p className={`fw-bold severity-label w-100
${props.uptime.isUp ? 'low' : (props.uptime.undergoingMaintenance ? 'med' : 'high')}-severity`}
>
{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", height: '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>{new Date().getFullYear()} Uptime:</b> {props.uptime.uptimeYtd}%</p>
<p className={"col-md-6 col-12"}><b>Avg Response Time:</b> {props.uptime.averageResponseTime}ms</p>
<Modal className={"uptime-modal"} isOpen={isModalOpen}>
<div className={"mb-3"}>
<h1 className={"mb-0"}>{props.uptime.name}</h1>
{props.uptime.url && <p>(<a href={props.uptime.url}>{props.uptime.url}</a>)</p>}
</div>
<div className={"mb-3"}>
<p>Monitoring since {props.uptime.monitorStart}</p>
<p><b>{new Date().getFullYear()} Uptime (YTD):</b> {props.uptime.uptimeYtd}%</p>
<p><b>Last Month Uptime:</b> {props.uptime.uptimeMonth}%</p>
<p><b>All-Time Uptime:</b> {props.uptime.uptimeAllTime}%</p>
<p><b>Average Response Time:</b> {props.uptime.averageResponseTime}ms</p>
</div>
{
props.isService &&
<div className={"mb-3"}>
<i>Note that the uptime and performance of services hosted on NWS may be affected by factors not controlled by NWS such as bad bad optimization or buggy software.</i>
</div>
}
<button className={"w-100"} onClick={()=>setModalOpen(false)}>Close</button>
</Modal>
</div>
);
}

View file

@ -1,90 +0,0 @@
import {UptimeRecord} from "../nws-api/types";
import React, {useState} from "react";
import '../App.css';
import "./UptimeCard.css"
import Modal from "react-modal";
function getUptimeClass(uptime: number) {
if(uptime === 100) {
return "stat-perfect";
}
if(uptime >= 95) {
return "stat-middle";
}
return "stat-bad";
}
function getResponseTimeClass(respTime: number) {
// https://www.littledata.io/average/server-response-time
if(respTime < 205) {
return "stat-perfect";
}
if(respTime < 495) {
return "stat-middle";
}
return "stat-bad";
}
export default function UptimeComparisonCard(props: {uptime: UptimeRecord, isService: boolean}) {
const [isModalOpen, setModalOpen] = useState(false);
return(
<div className={"col-12 col-lg-2 mb-2 p-lg-0 m-lg-0 m-2 text-center nws-card-md"}>
<div style={{height: 25, margin: 0}} className={"pt-2 pt-lg-0"}>
<h4 className={"uptime-lnk"} onClick={()=>setModalOpen(true)}>{props.uptime.name}</h4>
</div>
<hr className={" w-100"}/>
<p className={"fw-bold d-lg-none"}>Uptime (Last Month)</p>
<div style={{height: 25, margin: 0}} className={"pt-2 pt-lg-0"}>
<p className={getUptimeClass(props.uptime.uptimeMonth)}>{props.uptime.uptimeMonth}%</p>
</div>
<hr className={"d-lg-block d-none w-100"}/>
<p className={"fw-bold d-lg-none"}>Uptime ({new Date().getFullYear()} YTD)</p>
<div style={{height: 25, margin: 0}} className={"pt-2 pt-lg-0"}>
<p className={getUptimeClass(props.uptime.uptimeYtd)}>{props.uptime.uptimeYtd}%</p>
</div>
<hr className={"d-lg-block d-none w-100"}/>
<p className={"fw-bold d-lg-none"}>Avg Response Time (24hr)</p>
<div style={{height: 25, margin: 0}} className={"pt-2 pt-lg-0"}>
<p className={getResponseTimeClass(props.uptime.averageResponseTime)}>{props.uptime.averageResponseTime}ms</p>
</div>
<hr className={"d-lg-block d-none w-100"} />
<p className={"fw-bold d-lg-none"}>Current Status</p>
<div style={{height: 25, margin: 0}} className={"pt-2 pt-lg-0"}>
<div className={`p-1 d-flex justify-content-start w-100`} >
<p className={`fw-bold severity-label w-100
${props.uptime.isUp ? 'low' : (props.uptime.undergoingMaintenance ? 'med' : 'high')}-severity`}
>
{props.uptime.isUp ? 'Up' : (props.uptime.undergoingMaintenance ? 'Maintenance' : 'Down')}
</p>
</div>
</div>
<Modal className={"uptime-modal"} isOpen={isModalOpen}>
<div className={"mb-3"}>
<h1 className={"mb-0"}>{props.uptime.name}</h1>
{props.uptime.url && <p>(<a href={props.uptime.url}>{props.uptime.url}</a>)</p>}
</div>
<div className={"mb-3"}>
<p>Monitoring since {props.uptime.monitorStart}</p>
<p><b>{new Date().getFullYear()} Uptime (YTD):</b> {props.uptime.uptimeYtd}%</p>
<p><b>Last Month Uptime:</b> {props.uptime.uptimeMonth}%</p>
<p><b>All-Time Uptime:</b> {props.uptime.uptimeAllTime}%</p>
<p><b>Average Response Time:</b> {props.uptime.averageResponseTime}ms</p>
</div>
{
props.isService &&
<div className={"mb-3"}>
<i>Note that the uptime and performance of services hosted on NWS may be affected by factors not controlled by NWS such as bad bad optimization or buggy software.</i>
</div>
}
<button className={"w-100"} onClick={()=>setModalOpen(false)}>Close</button>
</Modal>
</div>
);
}

View file

@ -1,35 +0,0 @@
import {UptimeRecord} from "../nws-api/types";
import React, {useState} from "react";
import '../App.css';
import "./UptimeCard.css"
import Modal from "react-modal";
export default function UptimeLabelCard() {
return(
<div className={"col-2 p-0 d-none d-lg-block mb-2 m-0 text-center"}>
<div style={{height: 25, margin: 0}}>
<p className={"fw-bold"} style={{fontSize: ".9em"}}>Service Name</p>
</div>
<hr className={"w-100"}/>
<div style={{height: 25, margin: 0}}>
<p className={"fw-bold"} style={{fontSize: ".9em"}}>Uptime (Last Month)</p>
</div>
<hr className={"w-100"}/>
<div style={{height: 25, margin: 0}}>
<p className={"fw-bold"} style={{fontSize: ".9em"}}>Uptime ({new Date().getFullYear()} YTD)</p>
</div>
<hr className={"w-100"}/>
<div style={{height: 25, margin: 0}}>
<p className={"fw-bold"} style={{fontSize: ".9em"}}>Avg Response Time (24hr)</p>
</div>
<hr className={"w-100"}/>
<div style={{height: 25, margin: 0}}>
<p className={"fw-bold"} style={{fontSize: ".9em"}}>Current Status</p>
</div>
</div>
);
}

View file

@ -1,32 +0,0 @@
.verify-box {
border-style: solid;
border-width: 1px;
border-color: #aaa;
border-radius: 10px;
align-self: center;
justify-self: center;
width: 500px;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: center;
}
.verify-label {
margin: 0;
}
.verify-button {
border-radius: 10px;
border-width: 0px;
background-color: cornflowerblue;
color: white;
margin-top: 10px;
margin-bottom: 10px;
padding: 3px
}

View file

@ -1,67 +0,0 @@
import "./RegisterPage.css";
import {useEffect, useState} from "react";
import {Account, SessionKey} from "../nws-api/types";
import {useSearchParams} from "react-router-dom";
import {Session} from "inspector";
export default function VerifyPage() {
const [pageState, setPageState] = useState<string>("");
const [searchParams, setSearchParams] = useSearchParams();
useEffect(()=>{
let verificationKey: string | null = searchParams.get("key");
if(verificationKey == null) {
setPageState("invalid_code");
return;
} else {
fetch("https://api-nws.nickorlow.com/Account/verify?verificationKey=" + verificationKey, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
}
}).then((result) => {
if (result.status == 200) {
result.json().then((o: SessionKey)=>{
localStorage.setItem("session_key", JSON.stringify(o));
window.location.href = '/dashboard';
});
}
if (result.status == 500) {
setPageState("server_error");
} else {
result.json().then((o) => {
if (o.ErrorMessage == "Invalid verification key.") {
setPageState("invalid_code");
} else {
setPageState("expired_code");
}
});
}
}).catch((e) => {
setPageState("invalid_code");
});
}
}, [])
return(
<div style={{minHeight: "100vh", display: "grid", width: "100%"}}>
<div className={"reg-box"} style={{display: pageState == "invalid_code" ? "flex" : "none"}}>
<h3>Uh Oh!</h3>
<p>Looks like the verification code you provided didn't work!</p>
<p className={"mt-2"}>Try to click on the link in the E-Mail sent to you instead of copying it.</p>
</div>
<div className={"reg-box"} style={{display: pageState == "expired_code" ? "flex" : "none"}}>
<h3>Expired Link</h3>
<p>It looks like the link you used to verify your account has expired.</p>
<p className={"mt-2"}>We've sent a new link to your email that is valid for 30 minutes.</p>
</div>
</div>
);
}

View file

@ -1,26 +0,0 @@
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;
}
.nav-lnk {
font-weight: bold;
color: black;
text-decoration: none;
padding-left: 30px;
font-size: 1.1rem;
transition: .5s;
}
.nav-lnk:hover {
color: #F7BA00;
}

View file

@ -1,179 +0,0 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import {
createBrowserRouter, NavLink,
RouterProvider,
} from "react-router-dom";
import StatusPage from "./components/StatusPage";
import reportWebVitals from "./reportWebVitals";
import "./index.css";
import 'bootstrap/dist/css/bootstrap.min.css';
import UptimeCard from "./components/UptimeCard";
import Footer from "./components/Footer";
import {Nav, Navbar, NavbarBrand, NavDropdown} from "react-bootstrap";
import NWSLogo from "./static/images/NWS_Logo_Transparent.png";
import Blogs from "./components/Blogs";
import NotFoundPage from "./components/NotFoundPage";
import LoginPage from "./components/LoginPage";
import RegisterPage from "./components/RegisterPage";
import VerifyPage from "./components/VerifyPage";
import DashboardPage from "./components/DashboardPage";
import CreateCruisePage from "./components/CreateCruisePage";
import HomePage from "./components/HomePage";
import NavbarCollapse from "react-bootstrap/NavbarCollapse";
import NavbarOffcanvas from "react-bootstrap/NavbarOffcanvas";
function Layout (props: {children: any}) {
return (
<div>
<header className={"w-100 sticky-top"}>
<div className={"w-100 d-flex justify-content-center align-content-center text-center p-1 "} style={{backgroundColor: "#004C54", height: 30}} >
<p className={"text-white fw-bold"}>Fly Eagles Fly</p>
<img src={"https://logos-world.net/wp-content/uploads/2020/05/Philadelphia-Eagles-Logo.png"} style={{maxHeight: "100%", maxWidth: "100%"}}/>
</div>
<div className={"w-100"}>
<Navbar sticky={"top"} expand="lg" className={"row justify-content-center m-0 p-0"} style={{backgroundColor: "#eee"}}>
<div className={"row w-100"}>
<div className="row w-100 d-md-none d-sm-block">
<div className={"col-9"}>
<Navbar.Brand href="/">
<img src={NWSLogo} width={150}/>
</Navbar.Brand>
</div>
<div className={"col-2 ml-3 d-flex align-content-center justify-content-center"}>
<Navbar.Toggle className={"h-50 align-self-center"} aria-controls="basic-navbar-nav"/>
</div>
</div>
<div className={"d-md-block d-none col-2"}>
<Navbar.Brand href="/">
<img src={NWSLogo} width={150}/>
</Navbar.Brand>
</div>
<Navbar.Collapse id="basic-navbar-nav" className={"col-10"}>
<Nav className="row w-100 ml-5">
<div className="col-md-4 row">
<NavLink className="col-sm-12 col-md-3 nav-lnk align-self-center" to={"/"}>Home</NavLink>
<NavLink className="col-sm-12 col-md-3 nav-lnk align-self-center" to={"/status"}>Status</NavLink>
</div>
<div className="col-md-6"/>
<div className={"col-md-2 d-md-block d-none"}>
{ localStorage.getItem("session_key") === null &&
(
<NavLink className={"nav-lnk"} to={"/login"}>
Login
</NavLink>
)
}
{ localStorage.getItem("session_key") === null ||
(
<NavDropdown title={"Account"} className={"nav-lnk"}>
<NavLink className={"nav-lnk"} to={"/dashboard"}>
Dashboard
</NavLink>
<hr/>
<NavLink className={"nav-lnk"} to={"/login"} onClick={()=>{localStorage.removeItem("session_key")}}>
Logout
</NavLink>
</NavDropdown>
)
}
</div>
</Nav>
</Navbar.Collapse>
</div>
</Navbar>
</div>
</header>
<div style={{minHeight: "92vh"}}>
{props.children}
</div>
<Footer/>
</div>
);
}
const router = createBrowserRouter([
{
path: "/",
element:
<Layout>
<HomePage/>
</Layout>
},
{
path: "status",
element:
<Layout>
<StatusPage/>
</Layout>
},
{
path: "blog",
element:
<Layout>
<Blogs/>
</Layout>
},
{
path: "blogs",
element:
<Layout>
<Blogs/>
</Layout>
},
{
path: "login",
element:
<Layout>
<LoginPage/>
</Layout>
},
{
path: "verify",
element:
<Layout>
<VerifyPage/>
</Layout>
},
{
path: "dashboard",
element:
<Layout>
<DashboardPage/>
</Layout>
},
{
path: "register",
element:
<Layout>
<RegisterPage/>
</Layout>
},
{
path: "cruise/new",
element:
<Layout>
<CreateCruisePage/>
</Layout>
},
{
path: "*",
element:
<Layout>
<NotFoundPage/>
</Layout>
},
]);
// @ts-ignore
ReactDOM.createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />
);
// 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();

View file

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2.6 KiB

272
src/main.rs Normal file
View file

@ -0,0 +1,272 @@
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,
})
}

View file

@ -1,51 +0,0 @@
import {Blog, Incident, Namespace, Service, SessionKey, 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');
try {
let incidents: Incident[] = await response.json();
if(incidents === null || incidents === undefined || !Array.isArray(incidents)) return [];
return incidents;
} catch (e) {
return [];
}
}
export async function getBlogs(): Promise<Blog[]> {
let response: Response = await fetch('https://api-nws.nickorlow.com/blogs');
let blogs: Blog[] = await response.json();
return blogs;
}
export async function getSessionKey(accountId: string, password: string): Promise<SessionKey> {
let response: Response = await fetch('https://api-nws.nickorlow.com/Account/session',
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'id': accountId,
'password': password
})
});
let sessionKey: SessionKey = await response.json();
return sessionKey;
}
export async function getNamespaces(accountId: string, skey: SessionKey): Promise<Namespace[]> {
let response: Response = await fetch('https://api-nws.nickorlow.com/'+accountId+'/namespaces', {
headers: {
Authorization: skey.id
}
});
let namespaces: Namespace[] = await response.json();
return namespaces;
}

View file

@ -1,156 +0,0 @@
import {useEffect, useState} from "react";
import {Account, Namespace, Service, SessionKey} from "./types";
export function useNonLoggedInRedirect() {
useEffect(()=>{
let rawSession: string | null = localStorage.getItem("session_key");
if(rawSession != null) {
let session: SessionKey = JSON.parse(rawSession);
if(session.expiry < new Date()) {
localStorage.removeItem("session_key");
} else {
window.location.href = "/dashboard";
}
}
}, []);
return true;
}
export function useLoggedInRedirect() {
useEffect(()=>{
let rawSession: string | null = localStorage.getItem("session_key");
if(rawSession != null) {
let session: SessionKey = JSON.parse(rawSession);
if(session.expiry > new Date()) {
window.location.href = "/login";
}
} else {
window.location.href = "/login";
}
}, []);
return true;
}
export function useGetAccountServices() {
const [services, setService] = useState<Service[]>([]);
useEffect(() => {
let rawSession: string | null = localStorage.getItem("session_key");
if(rawSession != null) {
let session: SessionKey = JSON.parse(rawSession);
fetch("https://api-nws.nickorlow.com/Account/services?accountId=" + session.accountId,
{
headers: {
"Authorization": btoa(session.accountId + ":" + session.id)
}
}).then((response)=>{
response.json().then((svcs: Service[]) => {
console.log(svcs)
setService(svcs);
});
});
}
}, []);
return services;
}
export function useGetAccountNamespaces() {
const [namespaces, setNamespaces] = useState<Namespace[]>([]);
useEffect(() => {
let rawSession: string | null = localStorage.getItem("session_key");
if(rawSession != null) {
let session: SessionKey = JSON.parse(rawSession);
fetch("https://api-nws.nickorlow.com/Account/" + session.accountId + "/namespaces",
{
headers: {
"Authorization": btoa(session.accountId + ":" + session.id)
}
}).then((response)=>{
response.json().then((svcs: Namespace[]) => {
console.log(svcs)
setNamespaces(svcs);
});
});
}
}, []);
return namespaces;
}
export function useNWSAuthKey() {
const [key, setKey] = useState('');
useEffect(() => {
let rawSession: string | null = localStorage.getItem("session_key");
if(rawSession != null) {
let session: SessionKey = JSON.parse(rawSession);
setKey(btoa(session.accountId + ":" + session.id))
}
}, []);
return key;
}
export function useGetServicesInNamespace() {
const [services, setServices] = useState<Service[]>([]);
const [ns, setNs] = useState<Namespace | null>(null);
useEffect(() => {
console.log(ns !== null ? ns.id : "null")
if(ns === null) return;
let rawSession: string | null = localStorage.getItem("session_key");
if(rawSession != null) {
let session: SessionKey = JSON.parse(rawSession);
fetch("https://api-nws.nickorlow.com/Account/" + session.accountId + "/namespaces/" + ns.id + "/services",
{
headers: {
"Authorization": btoa(session.accountId + ":" + session.id)
}
}).then((response)=>{
response.json().then((svcs: Service[]) => {
console.log(svcs)
setServices(svcs);
});
});
}
}, [ns]);
return {setNs, services, ns};
}
export function useNWSAccount() {
const [accountInfo, setAccountInfo] = useState<Account>();
useEffect(()=>{
let rawSession: string | null = localStorage.getItem("session_key");
if(rawSession != null) {
let session: SessionKey = JSON.parse(rawSession);
fetch("https://api-nws.nickorlow.com/Account/"+session.accountId, {
headers: {
"Authorization": btoa(session.accountId+":"+session.id)
}
}).then((e)=>{
if(e.status == 200) {
e.json().then((o: Account) => {
setAccountInfo(o)
})
} else {
localStorage.removeItem("session_key");
window.location.href = "/login";
}
});
} else {
localStorage.removeItem("session_key");
window.location.href = "/login";
}
}, []);
return accountInfo;
}

View file

@ -1,75 +0,0 @@
export type UptimeRecord = {
name: string,
url: string,
uptimeMonth: number,
uptimeAllTime: number,
uptimeYtd: number,
averageResponseTime: number,
monitorStart: string,
isUp: boolean,
undergoingMaintenance: boolean
};
export type UptimeResponse = {
datacenters: UptimeRecord[],
services: UptimeRecord[],
competitors: 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
};
// Below is primarily for user-facing things
export type Account = {
id?: string,
email: string,
name?: string,
password?: string,
status?: string
};
export type Service = {
serviceId: string,
serviceName: string,
namespace: string,
containerUrl: string,
ownerId: string
}
export type ApiError = {
StatusCode: number,
ErrorMessage: string
};
export type SessionKey = {
id: string,
expiry: Date,
accountId: string,
ip: string
};
export type Namespace = {
id: string,
accountId: string,
name: string
}

View file

@ -1 +0,0 @@
/// <reference types="react-scripts" />

View file

@ -1,15 +0,0 @@
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;

View file

@ -1,5 +0,0 @@
// 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';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

250
src/uptime_service.rs Normal file
View file

@ -0,0 +1,250 @@
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(())
}
}

18
templates/blog.html Normal file
View file

@ -0,0 +1,18 @@
<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 %}

View file

@ -0,0 +1,89 @@
<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>

View file

@ -0,0 +1,51 @@
<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>

View file

@ -0,0 +1,9 @@
<h1>Goodbye, NWS</h1>
<p>
<b>
Nick Web Services (NWS) is now Nick Web Services (NWS).
</b>
</p>
<p>That is all</p>

2
templates/dashboard.html Normal file
View file

@ -0,0 +1,2 @@
<h1>Under Construction</h1>
<p>The new dashboard isn't ready yet! Nobody but me used it anyways!</p>

31
templates/index.html Normal file
View file

@ -0,0 +1,31 @@
{%- 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>

54
templates/layout.html Normal file
View file

@ -0,0 +1,54 @@
<!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 &#169; <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>

View file

View file

@ -0,0 +1,26 @@
{%- 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) %}

View file

@ -0,0 +1,35 @@
{% 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 %}

View file

@ -1,26 +0,0 @@
{
"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"
]
}