mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-27 16:54:04 +00:00
Merge branch 'master' into 2.0-last-part
This commit is contained in:
commit
696d902983
10 changed files with 193 additions and 59 deletions
|
@ -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)
|
|
|
@ -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]
|
|
||||||
}
|
|
|
@ -17,7 +17,6 @@ README.md
|
||||||
.vscode
|
.vscode
|
||||||
.eslint*
|
.eslint*
|
||||||
.stylelint*
|
.stylelint*
|
||||||
/.devcontainer
|
|
||||||
/.github
|
/.github
|
||||||
yarn.lock
|
yarn.lock
|
||||||
app.json
|
app.json
|
||||||
|
|
|
@ -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/))
|
- 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/))
|
- 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
|
## Git Branches
|
||||||
|
|
||||||
- `master`: 2.X.X development. If you want to add a new feature, your pull request should base on this.
|
- `master`: 2.X.X development. If you want to add a new feature, your pull request should base on this.
|
||||||
|
|
25
package-lock.json
generated
25
package-lock.json
generated
|
@ -31,6 +31,7 @@
|
||||||
"express": "~4.19.2",
|
"express": "~4.19.2",
|
||||||
"express-basic-auth": "~1.2.1",
|
"express-basic-auth": "~1.2.1",
|
||||||
"express-static-gzip": "~2.1.7",
|
"express-static-gzip": "~2.1.7",
|
||||||
|
"feed": "^4.2.2",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"gamedig": "^4.2.0",
|
"gamedig": "^4.2.0",
|
||||||
"html-escaper": "^3.0.3",
|
"html-escaper": "^3.0.3",
|
||||||
|
@ -8315,6 +8316,18 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
"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": {
|
"node_modules/xmlbuilder": {
|
||||||
"version": "8.2.2",
|
"version": "8.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
|
||||||
|
|
|
@ -94,6 +94,7 @@
|
||||||
"express": "~4.19.2",
|
"express": "~4.19.2",
|
||||||
"express-basic-auth": "~1.2.1",
|
"express-basic-auth": "~1.2.1",
|
||||||
"express-static-gzip": "~2.1.7",
|
"express-static-gzip": "~2.1.7",
|
||||||
|
"feed": "^4.2.2",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"gamedig": "^4.2.0",
|
"gamedig": "^4.2.0",
|
||||||
"html-escaper": "^3.0.3",
|
"html-escaper": "^3.0.3",
|
||||||
|
|
|
@ -5,6 +5,10 @@ const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const jsesc = require("jsesc");
|
const jsesc = require("jsesc");
|
||||||
const googleAnalytics = require("../google-analytics");
|
const googleAnalytics = require("../google-analytics");
|
||||||
const { marked } = require("marked");
|
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 {
|
class StatusPage extends BeanModel {
|
||||||
|
|
||||||
|
@ -14,6 +18,24 @@ class StatusPage extends BeanModel {
|
||||||
*/
|
*/
|
||||||
static domainMappingList = { };
|
static domainMappingList = { };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle responses to RSS pages
|
||||||
|
* @param {Response} response Response object
|
||||||
|
* @param {string} slug Status page slug
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
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
|
* Handle responses to status page
|
||||||
* @param {Response} response Response object
|
* @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<string>} 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
|
* SSR for status pages
|
||||||
* @param {string} indexHTML HTML page to render
|
* @param {string} indexHTML HTML page to render
|
||||||
|
@ -98,6 +152,109 @@ class StatusPage extends BeanModel {
|
||||||
return $.root().html();
|
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
|
* Get all status page data in one call
|
||||||
* @param {StatusPage} statusPage Status page to get data for
|
* @param {StatusPage} statusPage Status page to get data for
|
||||||
|
|
|
@ -18,6 +18,11 @@ router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
|
||||||
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
|
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) => {
|
router.get("/status", cache("5 minutes"), async (request, response) => {
|
||||||
let slug = "default";
|
let slug = "default";
|
||||||
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
|
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
|
||||||
|
|
|
@ -43,12 +43,15 @@
|
||||||
<div v-if="!isCollapsed" class="childs">
|
<div v-if="!isCollapsed" class="childs">
|
||||||
<MonitorListItem
|
<MonitorListItem
|
||||||
v-for="(item, index) in sortedChildMonitorList"
|
v-for="(item, index) in sortedChildMonitorList"
|
||||||
:key="index" :monitor="item"
|
:key="index"
|
||||||
|
:monitor="item"
|
||||||
:isSelectMode="isSelectMode"
|
:isSelectMode="isSelectMode"
|
||||||
:isSelected="isSelected"
|
:isSelected="isSelected"
|
||||||
:select="select"
|
:select="select"
|
||||||
:deselect="deselect"
|
:deselect="deselect"
|
||||||
:depth="depth + 1"
|
:depth="depth + 1"
|
||||||
|
:filter-func="filterFunc"
|
||||||
|
:sort-func="sortFunc"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
|
@ -6,6 +6,7 @@ const languageList = {
|
||||||
"cs-CZ": "Čeština",
|
"cs-CZ": "Čeština",
|
||||||
"zh-HK": "繁體中文 (香港)",
|
"zh-HK": "繁體中文 (香港)",
|
||||||
"bg-BG": "Български",
|
"bg-BG": "Български",
|
||||||
|
"be": "Беларуская",
|
||||||
"de-DE": "Deutsch (Deutschland)",
|
"de-DE": "Deutsch (Deutschland)",
|
||||||
"de-CH": "Deutsch (Schweiz)",
|
"de-CH": "Deutsch (Schweiz)",
|
||||||
"nl-NL": "Nederlands",
|
"nl-NL": "Nederlands",
|
||||||
|
|
Loading…
Reference in a new issue