commit a55ccd4335ed92551602f111c103627b44308d25 Author: Sofía Aritz Date: Sat Oct 28 18:28:50 2023 +0200 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..789eeba --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +./database/ +node_modules/ \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b5259f4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.{ts,js,tsx,jsx}] +indent_style = tab +tab_width = 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..459adde --- /dev/null +++ b/.gitignore @@ -0,0 +1,181 @@ +# Application specific +database/database.json + +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +# Bun +bun.lockb diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2a5f3bf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM oven/bun:1 + +ENV LISTEN_PORT 7000 + +WORKDIR . +COPY . . + +RUN bun install +CMD ["bun", "start"] +EXPOSE ${LISTEN_PORT} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..899cf8e --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Status Page +This is a simple way to create customizable status pages. + diff --git a/database/example.json5 b/database/example.json5 new file mode 100644 index 0000000..5d7eabe --- /dev/null +++ b/database/example.json5 @@ -0,0 +1,87 @@ +// NOTE: JSON5 is not supported! Change the extension to `json` and remove the comments. +// NOTE: This example _may be_ obsolete. Check [src/db.ts] for the full schema. +{ + "metadata": { + "title": "Example" + }, + "statuses": { + "working": { + "displayName": "Working", + "color": "#26a269", + "description": "The service is up and running" + }, + "unstable": { + "displayName": "Unstable", + "color": "#e5a50a", + "description": "The service may have some short-lived problems" + }, + "degraded": { + "displayName": "Degraded", + "color": "#c64600", + "description": "There are known issues being resolved right now" + }, + "down": { + "display": "Down", + "color": "#a51d2d", + "description": "The service is down" + }, + "deprecated": { + "display": "Deprecated", + "color": "#613583", + "description": "Status of this service is not tracked and it may be turned off at any time" + } + }, + "notices": [ + { + "title": "Maintenance of the CDN", + "expectedStatus": "unstable", // Optional + "affectedServices": ["cdn", "invidious"], // Optional + "expectedDuration": { // Optional + "from": 1698408000000, // Result of Date.UTC(...) + "to": 1698494400000 + }, + "description": "The CDN is going to be in maintenance for some time" + } + ], + "services": { + "cdn": { + "displayName": "CDN", + "link": "https://cdn.sofiaritz.com", // The link is optional + "description": "CDN used to host things like GFonts", + "status": "working" + }, + "invidious": { + "displayName": "Invidious", + "description": "Invite-only Invidious instance", + "status": "unstable", + "updates": [ + { + "time": 1698348215919, + "description": "**Monitoring** ‐ A fix has been implemented and the result are being monitored.", + "author": "Sofía Aritz" // The author is optional + }, + { + "time": 169834801889, // The result of `Date.now()` + "description": "**Investigating** ‐ Log-in is not working properly." + } + ] + } + }, + "contact": [ + { + "name": "Sofía Aritz", + "links": [ + { + "name": "E-Mail", + "href": "mailto:sofi@sofiaritz.com" + }, + { + "name": "Matrix", + "href": "https://matrix.to/#/@sofiaritz:matrix.org" + } + ], + "gravatarEmail": "sofi@sofiaritz.com", // This is optional + "additionalInfo": "My PGP key is available at [sofiaritz.com/keys/pub.asc](https://sofiaritz.com/keys/pub.asc)" // This is optional + } + ] +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ca0c95c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3" + +services: + status-page: + image: "status-page:latest" + volumes: + - ./database:/home/bun/app/database:ro + ports: + - "7000:7000" \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..6034d98 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "status-page", + "module": "src/index.ts", + "type": "module", + "scripts": { + "start": "bun run src/index.ts", + "watch": "bun --watch src/index.ts", + "docker:build": "docker build -t status-page ." + }, + "devDependencies": { + "bun-types": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@elysiajs/html": "^0.7.3", + "@elysiajs/static": "^0.7.1", + "elysia": "^0.7.21", + "handlebars": "^4.7.8", + "lowdb": "^6.1.1" + } +} \ No newline at end of file diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..99735fb --- /dev/null +++ b/public/style.css @@ -0,0 +1,103 @@ +:root { + font-family: Inter, "Segoe UI", system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +h1, h2, h3, h4, h5, h6, p { + margin: 0; +} + +ul, ol { + margin-top: 0; + margin-bottom: 0; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +summary:hover { + cursor: pointer; +} + +main { + display: flex; + flex-direction: column; + gap: 1rem; +} + +main > div { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.notices { + border: #fafafa solid 1px; + padding: .5rem; + border-radius: 5px; +} + +.notice { + margin: 1rem 0; +} + +.expected-status, .affected-services, .expected-duration { + padding-left: 1rem; +} + +.services { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; +} + +.service, .status { + border: #fafafa solid 1px; + padding: 1rem; + border-radius: 5px; +} + +.contact, .statuses { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.individual-contact { + border: #fafafa solid 1px; + padding: 1rem; + border-radius: 5px; +} + +.contact-metadata, .contact-links { + display: flex; + align-items: center; + gap: 1rem; +} + +.contact-metadata img { + border-radius: 99999px; +} + +@media only screen and (max-width: 1000px) { + .services { + display: flex; + flex-direction: column; + } +} \ No newline at end of file diff --git a/src/consts.ts b/src/consts.ts new file mode 100644 index 0000000..04a7525 --- /dev/null +++ b/src/consts.ts @@ -0,0 +1,2 @@ +export const UPDATE_INTERVAL = Number(Bun.env["UPDATE_INTERVAL"]) || 1000 +export const LISTEN_PORT = Number(Bun.env["LISTEN_PORT"]) || 7000 \ No newline at end of file diff --git a/src/db.ts b/src/db.ts new file mode 100644 index 0000000..a51387c --- /dev/null +++ b/src/db.ts @@ -0,0 +1,74 @@ +import { JSONPreset } from "lowdb/node" +import {UPDATE_INTERVAL} from "./consts.ts"; + +const defaultData: Database = { + metadata: { + title: "Default Status Page" + }, + statuses: { + defaultData: { + displayName: "Default Data", + color: "#c64600", + description: "This status is part of the default schema" + } + }, + services: { + example: { + displayName: "Example", + description: "This service is an example", + status: "defaultData" + } + }, + contact: [{ + name: "Example maintainer", + links: [{ + name: "Example contact", + href: "mailto:example@example.com" + }], + additionalInfo: "This is an example contact info" + }] +} +export const database = await JSONPreset("database/database.json", defaultData) + +setInterval(() => database.read(), UPDATE_INTERVAL) + +export type Database = { + metadata: { + title: string, + }, + statuses: {[key: string]: { + displayName: string, + color: string, + description: string, + }}, + notices?: { + title: string, + description: string, + expectedStatus?: keyof Database["statuses"], + affectedServices?: (keyof Database["services"])[], + expectedDuration?: { + from: number, + to: number, + }, + }[], + services: {[key: string]: { + displayName: string, + description: string, + status: keyof Database["statuses"], + link?: string, + updates?: { + time: number, + description: string, + author?: string, + }[], + }}, + contact: { + name: string, + links: { + name: string, + href: string, + }[], + gravatarEmail?: string, + additionalInfo?: string, + }[], +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..1e7d586 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,46 @@ +import {database} from "./db.ts"; +import Elysia from "elysia"; +import Handlebars from "handlebars"; +import {readFile} from "fs/promises"; +import {LISTEN_PORT} from "./consts.ts"; +import {html} from "@elysiajs/html"; +import staticPlugin from "@elysiajs/static"; +import {hash} from "bun"; + +Handlebars.registerHelper("status", (status, options) => { + return options.fn({ + status, + ...options.data.root["statuses"][status] + }) +}) + +Handlebars.registerHelper("inlineStatus", (statusID, options) => { + let status = options.data.root["statuses"][statusID] + return new Handlebars.SafeString(`${status.displayName}`) +}) + +Handlebars.registerHelper("inlineService", (serviceID, options) => { + let service = options.data.root["services"][serviceID] + return new Handlebars.SafeString(`${service.displayName}`) +}) + +Handlebars.registerHelper("epochUTC", (time) => { + return new Handlebars.SafeString(``) +}) + +Handlebars.registerHelper("gravatar", (email) => { + let hasher = new Bun.CryptoHasher("sha256") + hasher.update(email.trim().toLowerCase()) + let src = "https://gravatar.com/avatar/" + hasher.digest("hex") + + return new Handlebars.SafeString(``) +}) + +let template = Handlebars.compile((await readFile("templates/index.hbs")).toString()) +new Elysia() + .use(html()) + .use(staticPlugin()) + .get("/", () => template(database.data)) + .listen(LISTEN_PORT) + +console.log(`Status Page is running on port ${LISTEN_PORT}`) \ No newline at end of file diff --git a/templates/index.hbs b/templates/index.hbs new file mode 100644 index 0000000..6591d7c --- /dev/null +++ b/templates/index.hbs @@ -0,0 +1,121 @@ + + + + + + + {{metadata.title}} + + +
+ {{#if notices}} +
+

Notices

+
+ {{#each notices}} +
+

{{title}}

+ {{#if expectedStatus}} +
+ Expected status: {{inlineStatus expectedStatus}} +
+ {{/if}} + {{#if affectedServices.length}} +
+ Affected services: + {{#each affectedServices}} + {{inlineService this}} ⊣ + {{/each}} +
+ {{/if}} + {{#if expectedDuration}} +
+ Expected duration +
    +
  • From: {{epochUTC expectedDuration.from}}
  • +
  • To: {{epochUTC expectedDuration.to}}
  • +
+
+ {{/if}} +

{{{description}}}

+
+ {{/each}} +
+
+ {{/if}} +
+

Services

+
+ {{#each services}} +
+

{{displayName}}

+ +

{{{description}}}

+ {{#if updates.length}} +
+
+ {{#each updates}} +
+ Updates +
+ +

{{{description}}}

+
+
+ {{/each}} +
+ {{/if}} +
+ {{/each}} +
+
+
+

Contact

+
+ {{#each contact}} +
+ +

{{{additionalInfo}}}

+ +
+ {{/each}} +
+
+
+

Statuses

+
+ {{#each statuses}} +
+

{{displayName}}

+

{{description}}

+
+ {{/each}} +
+
+
+ + + \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7556e1d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "noEmit": true, + "composite": true, + "strict": true, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "types": [ + "bun-types" // add Bun global + ] + } +}