diff --git a/.devcontainer/README.md b/.devcontainer/README.md deleted file mode 100644 index 81b8b0fa0..000000000 --- a/.devcontainer/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Codespaces - -You can modifiy Uptime Kuma in your browser without setting up a local development. - -![image](https://github.com/louislam/uptime-kuma/assets/1336778/31d9f06d-dd0b-4405-8e0d-a96586ee4595) - -1. Click `Code` -> `Create codespace on master` -2. Wait a few minutes until you see there are two exposed ports -3. Go to the `3000` url, see if it is working - -![image](https://github.com/louislam/uptime-kuma/assets/1336778/909b2eb4-4c5e-44e4-ac26-6d20ed856e7f) - -## Frontend - -Since the frontend is using [Vite.js](https://vitejs.dev/), all changes in this area will be hot-reloaded. -You don't need to restart the frontend, unless you try to add a new frontend dependency. - -## Backend - -The backend does not automatically hot-reload. -You will need to restart the backend after changing something using these steps: - -1. Click `Terminal` -2. Click `Codespaces: server-dev` in the right panel -3. Press `Ctrl + C` to stop the server -4. Press `Up` to run `npm run start-server-dev` - -![image](https://github.com/louislam/uptime-kuma/assets/1336778/e0c0a350-fe46-4588-9f37-e053c85834d1) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 6e3282dc8..000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "image": "mcr.microsoft.com/devcontainers/javascript-node:dev-18-bookworm", - "features": { - "ghcr.io/devcontainers/features/github-cli:1": {} - }, - "updateContentCommand": "npm ci", - "postCreateCommand": "", - "postAttachCommand": { - "frontend-dev": "npm run start-frontend-devcontainer", - "server-dev": "npm run start-server-dev", - "open-port": "gh codespace ports visibility 3001:public -c $CODESPACE_NAME" - }, - "customizations": { - "vscode": { - "extensions": [ - "streetsidesoftware.code-spell-checker", - "dbaeumer.vscode-eslint", - "GitHub.copilot-chat" - ] - } - }, - "forwardPorts": [3000, 3001] -} diff --git a/.dockerignore b/.dockerignore index 4ef25f5eb..5db08b7bf 100644 --- a/.dockerignore +++ b/.dockerignore @@ -17,7 +17,6 @@ README.md .vscode .eslint* .stylelint* -/.devcontainer /.github yarn.lock app.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b37b5d85..69f98c0e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -236,12 +236,6 @@ The goal is to make the Uptime Kuma installation as easy as installing a mobile - IDE that supports [`ESLint`](https://eslint.org/) and EditorConfig (I am using [`IntelliJ IDEA`](https://www.jetbrains.com/idea/)) - A SQLite GUI tool (f.ex. [`SQLite Expert Personal`](https://www.sqliteexpert.com/download.html) or [`DBeaver Community`](https://dbeaver.io/download/)) -### GitHub Codespaces - -If you don't want to setup an local environment, you can now develop on GitHub Codespaces, read more: - -https://github.com/louislam/uptime-kuma/tree/master/.devcontainer - ## Git Branches - `master`: 2.X.X development. If you want to add a new feature, your pull request should base on this. diff --git a/package-lock.json b/package-lock.json index e2f6fbd5e..c5b2f54c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "express": "~4.19.2", "express-basic-auth": "~1.2.1", "express-static-gzip": "~2.1.7", + "feed": "^4.2.2", "form-data": "~4.0.0", "gamedig": "^4.2.0", "html-escaper": "^3.0.3", @@ -8315,6 +8316,18 @@ "dev": true, "license": "MIT" }, + "node_modules/feed": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", + "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", + "license": "MIT", + "dependencies": { + "xml-js": "^1.6.11" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -15925,6 +15938,18 @@ } } }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, "node_modules/xmlbuilder": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", diff --git a/package.json b/package.json index 6476b7410..52ce4cac1 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "express": "~4.19.2", "express-basic-auth": "~1.2.1", "express-static-gzip": "~2.1.7", + "feed": "^4.2.2", "form-data": "~4.0.0", "gamedig": "^4.2.0", "html-escaper": "^3.0.3", diff --git a/server/model/status_page.js b/server/model/status_page.js index e40b28f6f..38f548ebb 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -5,6 +5,10 @@ const { UptimeKumaServer } = require("../uptime-kuma-server"); const jsesc = require("jsesc"); const googleAnalytics = require("../google-analytics"); const { marked } = require("marked"); +const { Feed } = require("feed"); +const config = require("../config"); + +const { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE, DOWN } = require("../../src/util"); class StatusPage extends BeanModel { @@ -14,6 +18,24 @@ class StatusPage extends BeanModel { */ static domainMappingList = { }; + /** + * Handle responses to RSS pages + * @param {Response} response Response object + * @param {string} slug Status page slug + * @returns {Promise} + */ + static async handleStatusPageRSSResponse(response, slug) { + let statusPage = await R.findOne("status_page", " slug = ? ", [ + slug + ]); + + if (statusPage) { + response.send(await StatusPage.renderRSS(statusPage, slug)); + } else { + response.status(404).send(UptimeKumaServer.getInstance().indexHTML); + } + } + /** * Handle responses to status page * @param {Response} response Response object @@ -39,6 +61,38 @@ class StatusPage extends BeanModel { } } + /** + * SSR for RSS feed + * @param {statusPage} statusPage object + * @param {slug} slug from router + * @returns {Promise} the rendered html + */ + static async renderRSS(statusPage, slug) { + const { heartbeats, statusDescription } = await StatusPage.getRSSPageData(statusPage); + + let proto = config.isSSL ? "https" : "http"; + let host = `${proto}://${config.hostname || "localhost"}:${config.port}/status/${slug}`; + + const feed = new Feed({ + title: "uptime kuma rss feed", + description: `current status: ${statusDescription}`, + link: host, + language: "en", // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes + updated: new Date(), // optional, default = today + }); + + heartbeats.forEach(heartbeat => { + feed.addItem({ + title: `${heartbeat.name} is down`, + description: `${heartbeat.name} has been down since ${heartbeat.time}`, + id: heartbeat.monitorID, + date: new Date(heartbeat.time), + }); + }); + + return feed.rss2(); + } + /** * SSR for status pages * @param {string} indexHTML HTML page to render @@ -98,6 +152,109 @@ class StatusPage extends BeanModel { return $.root().html(); } + /** + * @param {heartbeats} heartbeats from getRSSPageData + * @returns {number} status_page constant from util.ts + */ + static overallStatus(heartbeats) { + if (heartbeats.length === 0) { + return -1; + } + + let status = STATUS_PAGE_ALL_UP; + let hasUp = false; + + for (let beat of heartbeats) { + if (beat.status === MAINTENANCE) { + return STATUS_PAGE_MAINTENANCE; + } else if (beat.status === UP) { + hasUp = true; + } else { + status = STATUS_PAGE_PARTIAL_DOWN; + } + } + + if (! hasUp) { + status = STATUS_PAGE_ALL_DOWN; + } + + return status; + } + + /** + * @param {number} status from overallStatus + * @returns {string} description + */ + static getStatusDescription(status) { + if (status === -1) { + return "No Services"; + } + + if (status === STATUS_PAGE_ALL_UP) { + return "All Systems Operational"; + } + + if (status === STATUS_PAGE_PARTIAL_DOWN) { + return "Partially Degraded Service"; + } + + if (status === STATUS_PAGE_ALL_DOWN) { + return "Degraded Service"; + } + + // TODO: show the real maintenance information: title, description, time + if (status === MAINTENANCE) { + return "Under maintenance"; + } + + return "?"; + } + + /** + * Get all data required for RSS + * @param {StatusPage} statusPage Status page to get data for + * @returns {object} Status page data + */ + static async getRSSPageData(statusPage) { + // get all heartbeats that correspond to this statusPage + const config = await statusPage.toPublicJSON(); + + // Public Group List + const showTags = !!statusPage.show_tags; + + const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [ + statusPage.id + ]); + + let heartbeats = []; + + for (let groupBean of list) { + let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry); + for (const monitor of monitorGroup.monitorList) { + const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [ monitor.id ]); + if (heartbeat) { + heartbeats.push({ + ...monitor, + status: heartbeat.status, + time: heartbeat.time + }); + } + } + } + + // calculate RSS feed description + let status = StatusPage.overallStatus(heartbeats); + let statusDescription = StatusPage.getStatusDescription(status); + + // keep only DOWN heartbeats in the RSS feed + heartbeats = heartbeats.filter(heartbeat => heartbeat.status === DOWN); + + return { + heartbeats, + statusDescription + }; + } + /** * Get all status page data in one call * @param {StatusPage} statusPage Status page to get data for diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index 42cccc942..b209d33d1 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -18,6 +18,11 @@ router.get("/status/:slug", cache("5 minutes"), async (request, response) => { await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug); }); +router.get("/status/:slug/rss", cache("5 minutes"), async (request, response) => { + let slug = request.params.slug; + await StatusPage.handleStatusPageRSSResponse(response, slug); +}); + router.get("/status", cache("5 minutes"), async (request, response) => { let slug = "default"; await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug); diff --git a/src/components/MonitorListItem.vue b/src/components/MonitorListItem.vue index 9b45ae9f2..74ba4835c 100644 --- a/src/components/MonitorListItem.vue +++ b/src/components/MonitorListItem.vue @@ -43,12 +43,15 @@
diff --git a/src/i18n.js b/src/i18n.js index f52c297c9..f415f5daf 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -6,6 +6,7 @@ const languageList = { "cs-CZ": "Čeština", "zh-HK": "繁體中文 (香港)", "bg-BG": "Български", + "be": "Беларуская", "de-DE": "Deutsch (Deutschland)", "de-CH": "Deutsch (Schweiz)", "nl-NL": "Nederlands",