diff --git a/compose.yaml b/compose.yaml index 004705a636..eea83c54f2 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,9 +1,18 @@ services: uptime-kuma: - image: louislam/uptime-kuma:1 + image: uptime-kuma:latest volumes: - ./data:/app/data ports: # : - 3001:3001 restart: unless-stopped + container_name: uptime-kuma + networks: + - anpr_network + extra_hosts: + - "host.docker.internal:host-gateway" + +networks: + anpr_network: + external: true diff --git a/db/knex_init_db.js b/db/knex_init_db.js index 7ce1bb48ee..461da82ed9 100644 --- a/db/knex_init_db.js +++ b/db/knex_init_db.js @@ -73,6 +73,8 @@ async function createTables() { .onUpdate("CASCADE"); table.integer("interval").notNullable().defaultTo(20); table.text("url"); + table.text("restart_url"); + table.integer("restart_interval"); table.string("type", 20); table.integer("weight").defaultTo(2000); table.string("hostname", 255); diff --git a/db/knex_migrations/2024-03-18-0000-add-restart-logic.js b/db/knex_migrations/2024-03-18-0000-add-restart-logic.js new file mode 100644 index 0000000000..77bf0aa2c8 --- /dev/null +++ b/db/knex_migrations/2024-03-18-0000-add-restart-logic.js @@ -0,0 +1,14 @@ +exports.up = function (knex) { + return knex.schema.alterTable("monitor", function (table) { + // Add new column monitor.remote_browser + table.integer("restart_interval").nullable().defaultTo(null); + table.string("restart_url", 255).nullable(); + }); +}; + +exports.down = function (knex) { + return knex.schema.alterTable("monitor", function (table) { + table.dropColumn("restart_interval"); + table.dropColumn("restart_url"); + }); +}; diff --git a/package-lock.json b/package-lock.json index f8b2ea8bb9..5d1f801e0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7151,12 +7151,6 @@ "node": ">=6" } }, - "node_modules/global-prefix/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, "node_modules/global-prefix/node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -7668,6 +7662,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", diff --git a/package.json b/package.json index 71b0d05ca6..69746597dd 100644 --- a/package.json +++ b/package.json @@ -209,4 +209,4 @@ "wait-on": "^7.2.0", "whatwg-url": "~12.0.1" } -} +} \ No newline at end of file diff --git a/server/model/monitor.js b/server/model/monitor.js index b2fed86f57..7b8812e4aa 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1,11 +1,37 @@ const dayjs = require("dayjs"); const axios = require("axios"); const { Prometheus } = require("../prometheus"); -const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, - SQL_DATETIME_FORMAT +const { + log, + UP, + DOWN, + PENDING, + MAINTENANCE, + flipStatus, + MAX_INTERVAL_SECOND, + MIN_INTERVAL_SECOND, + SQL_DATETIME_FORMAT, } = require("../../src/util"); -const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery, - redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal +const { + tcping, + ping, + checkCertificate, + checkStatusCode, + getTotalClientInRoom, + setting, + mssqlQuery, + postgresQuery, + mysqlQuery, + setSetting, + httpNtlm, + radius, + grpcQuery, + redisPingAsync, + mongodbPing, + kafkaProducerAsync, + getOidcTokenClientCredentials, + rootCertificatesFingerprints, + axiosAbortSignal, } = require("../util-server"); const { R } = require("redbean-node"); const { BeanModel } = require("redbean-node/dist/bean-model"); @@ -36,7 +62,6 @@ const rootCertificates = rootCertificatesFingerprints(); * 3 = MAINTENANCE */ class Monitor extends BeanModel { - /** * Return an object that ready to parse to JSON for public Only show * necessary data to public @@ -61,8 +86,15 @@ class Monitor extends BeanModel { obj.tags = await this.getTags(); } - if (certExpiry && (this.type === "http" || this.type === "keyword" || this.type === "json-query") && this.getURLProtocol() === "https:") { - const { certExpiryDaysRemaining, validCert } = await this.getCertExpiry(this.id); + if ( + certExpiry && + (this.type === "http" || + this.type === "keyword" || + this.type === "json-query") && + this.getURLProtocol() === "https:" + ) { + const { certExpiryDaysRemaining, validCert } = + await this.getCertExpiry(this.id); obj.certExpiryDaysRemaining = certExpiryDaysRemaining; obj.validCert = validCert; } @@ -77,7 +109,6 @@ class Monitor extends BeanModel { * @returns {object} Object ready to parse */ async toJSON(includeSensitiveData = true) { - let notificationIDList = {}; let list = await R.find("monitor_notification", " monitor_id = ? ", [ @@ -93,7 +124,10 @@ class Monitor extends BeanModel { let screenshot = null; if (this.type === "real-browser") { - screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png"; + screenshot = + "/screenshots/" + + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + + ".png"; } let data = { @@ -104,13 +138,15 @@ class Monitor extends BeanModel { parent: this.parent, childrenIDs: await Monitor.getAllChildrenIDs(this.id), url: this.url, + restartUrl: this.restartUrl, + restartInterval: this.restartInterval, method: this.method, hostname: this.hostname, port: this.port, maxretries: this.maxretries, weight: this.weight, active: await this.isActive(), - forceInactive: !await Monitor.isParentActive(this.id), + forceInactive: !(await Monitor.isParentActive(this.id)), type: this.type, timeout: this.timeout, interval: this.interval, @@ -153,7 +189,8 @@ class Monitor extends BeanModel { kafkaProducerTopic: this.kafkaProducerTopic, kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers), kafkaProducerSsl: this.getKafkaProducerSsl(), - kafkaProducerAllowAutoTopicCreation: this.getKafkaProducerAllowAutoTopicCreation(), + kafkaProducerAllowAutoTopicCreation: + this.getKafkaProducerAllowAutoTopicCreation(), kafkaProducerMessage: this.kafkaProducerMessage, screenshot, remote_browser: this.remote_browser, @@ -185,7 +222,9 @@ class Monitor extends BeanModel { tlsCa: this.tlsCa, tlsCert: this.tlsCert, tlsKey: this.tlsKey, - kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions), + kafkaProducerSaslOptions: JSON.parse( + this.kafkaProducerSaslOptions + ), }; } @@ -200,7 +239,7 @@ class Monitor extends BeanModel { async isActive() { const parentActive = await Monitor.isParentActive(this.id); - return (this.active === 1) && parentActive; + return this.active === 1 && parentActive; } /** @@ -209,7 +248,10 @@ class Monitor extends BeanModel { * monitor */ async getTags() { - return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ? ORDER BY tag.name", [ this.id ]); + return await R.getAll( + "SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ? ORDER BY tag.name", + [this.id] + ); } /** @@ -219,22 +261,24 @@ class Monitor extends BeanModel { * monitor */ async getCertExpiry(monitorID) { - let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ - monitorID, - ]); + let tlsInfoBean = await R.findOne( + "monitor_tls_info", + "monitor_id = ?", + [monitorID] + ); let tlsInfo; if (tlsInfoBean) { tlsInfo = JSON.parse(tlsInfoBean?.info_json); if (tlsInfo?.valid && tlsInfo?.certInfo?.daysRemaining) { return { certExpiryDaysRemaining: tlsInfo.certInfo.daysRemaining, - validCert: true + validCert: true, }; } } return { certExpiryDaysRemaining: "", - validCert: false + validCert: false, }; } @@ -333,10 +377,9 @@ class Monitor extends BeanModel { this.prometheus = new Prometheus(this); const beat = async () => { - let beatInterval = this.interval; - if (! beatInterval) { + if (!beatInterval) { beatInterval = 1; } @@ -352,14 +395,38 @@ class Monitor extends BeanModel { let tlsInfo = undefined; if (!previousBeat || this.type === "push") { - previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [ - this.id, - ]); + previousBeat = await R.findOne( + "heartbeat", + " monitor_id = ? ORDER BY time DESC", + [this.id] + ); if (previousBeat) { retries = previousBeat.retries; } } + // Restart the monitor if restartInterval is exceeded + const downSeconds = this.interval * previousBeat?.retries || 0; + if ( + this.restartUrl && + this.restartInterval && + this.restartUrl.length > 0 && + downSeconds >= this.restartInterval + ) { + log.info("monitor", `[${this.name}] Restarting monitor`); + axios + .get(this.restartUrl) + .then(() => { + log.info("monitor", `[${this.name}] Restarted monitor`); + }) + .catch((error) => { + log.error( + "monitor", + `[${this.name}] Failed to restart monitor: ${error.message}` + ); + }); + } + const isFirstBeat = !previousBeat; let bean = R.dispense("heartbeat"); @@ -380,42 +447,57 @@ class Monitor extends BeanModel { try { if (await Monitor.isUnderMaintenance(this.id)) { - bean.msg = "Monitor under maintenance"; + bean.msg = + "モニターメンテナンス中 / Monitor under maintenance"; bean.status = MAINTENANCE; } else if (this.type === "group") { const children = await Monitor.getChildren(this.id); if (children.length > 0) { bean.status = UP; - bean.msg = "All children up and running"; + bean.msg = + "すべてのプロセスが稼働中です / All children up and running"; for (const child of children) { if (!child.active) { // Ignore inactive childs continue; } - const lastBeat = await Monitor.getPreviousHeartbeat(child.id); + const lastBeat = await Monitor.getPreviousHeartbeat( + child.id + ); // Only change state if the monitor is in worse conditions then the ones before // lastBeat.status could be null if (!lastBeat) { bean.status = PENDING; - } else if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) { + } else if ( + bean.status === UP && + (lastBeat.status === PENDING || + lastBeat.status === DOWN) + ) { bean.status = lastBeat.status; - } else if (bean.status === PENDING && lastBeat.status === DOWN) { + } else if ( + bean.status === PENDING && + lastBeat.status === DOWN + ) { bean.status = lastBeat.status; } } if (bean.status !== UP) { - bean.msg = "Child inaccessible"; + bean.msg = + "AI モデルサーバーにアクセスできません / Child inaccessible"; } } else { // Set status pending if group is empty bean.status = PENDING; - bean.msg = "Group empty"; + bean.msg = "グループが空です / Group empty"; } - - } else if (this.type === "http" || this.type === "keyword" || this.type === "json-query") { + } else if ( + this.type === "http" || + this.type === "keyword" || + this.type === "json-query" + ) { // Do not do any queries/high loading things before the "bean.ping" let startTime = dayjs().valueOf(); @@ -423,7 +505,12 @@ class Monitor extends BeanModel { let basicAuthHeader = {}; if (this.auth_method === "basic") { basicAuthHeader = { - "Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass), + Authorization: + "Basic " + + this.encodeBase64( + this.basic_auth_user, + this.basic_auth_pass + ), }; } @@ -432,35 +519,59 @@ class Monitor extends BeanModel { let oauth2AuthHeader = {}; if (this.auth_method === "oauth2-cc") { try { - if (this.oauthAccessToken === undefined || new Date(this.oauthAccessToken.expires_at * 1000) <= new Date()) { - this.oauthAccessToken = await this.makeOidcTokenClientCredentialsRequest(); + if ( + this.oauthAccessToken === undefined || + new Date( + this.oauthAccessToken.expires_at * 1000 + ) <= new Date() + ) { + this.oauthAccessToken = + await this.makeOidcTokenClientCredentialsRequest(); } oauth2AuthHeader = { - "Authorization": this.oauthAccessToken.token_type + " " + this.oauthAccessToken.access_token, + Authorization: + this.oauthAccessToken.token_type + + " " + + this.oauthAccessToken.access_token, }; } catch (e) { - throw new Error("The oauth config is invalid. " + e.message); + throw new Error( + "The oauth config is invalid. " + e.message + ); } } const httpsAgentOptions = { maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) rejectUnauthorized: !this.getIgnoreTls(), - secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT, + secureOptions: + crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT, }; - log.debug("monitor", `[${this.name}] Prepare Options for axios`); + log.debug( + "monitor", + `[${this.name}] Prepare Options for axios` + ); let contentType = null; let bodyValue = null; - if (this.body && (typeof this.body === "string" && this.body.trim().length > 0)) { - if (!this.httpBodyEncoding || this.httpBodyEncoding === "json") { + if ( + this.body && + typeof this.body === "string" && + this.body.trim().length > 0 + ) { + if ( + !this.httpBodyEncoding || + this.httpBodyEncoding === "json" + ) { try { bodyValue = JSON.parse(this.body); contentType = "application/json"; } catch (e) { - throw new Error("Your JSON body is invalid. " + e.message); + throw new Error( + "Your JSON body is invalid. " + e.message + ); } } else if (this.httpBodyEncoding === "form") { bodyValue = this.body; @@ -477,15 +588,20 @@ class Monitor extends BeanModel { method: (this.method || "get").toLowerCase(), timeout: this.timeout * 1000, headers: { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", - ...(contentType ? { "Content-Type": contentType } : {}), - ...(basicAuthHeader), - ...(oauth2AuthHeader), - ...(this.headers ? JSON.parse(this.headers) : {}) + Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", + ...(contentType + ? { "Content-Type": contentType } + : {}), + ...basicAuthHeader, + ...oauth2AuthHeader, + ...(this.headers ? JSON.parse(this.headers) : {}), }, maxRedirects: this.maxredirects, validateStatus: (status) => { - return checkStatusCode(status, this.getAcceptedStatuscodes()); + return checkStatusCode( + status, + this.getAcceptedStatuscodes() + ); }, signal: axiosAbortSignal((this.timeout + 10) * 1000), }; @@ -498,9 +614,10 @@ class Monitor extends BeanModel { const proxy = await R.load("proxy", this.proxy_id); if (proxy && proxy.active) { - const { httpAgent, httpsAgent } = Proxy.createAgents(proxy, { - httpsAgentOptions: httpsAgentOptions, - }); + const { httpAgent, httpsAgent } = + Proxy.createAgents(proxy, { + httpsAgentOptions: httpsAgentOptions, + }); options.proxy = false; options.httpAgent = httpAgent; @@ -512,24 +629,37 @@ class Monitor extends BeanModel { let jar = new CookieJar(); let httpsCookieAgentOptions = { ...httpsAgentOptions, - cookies: { jar } + cookies: { jar }, }; - options.httpsAgent = new HttpsCookieAgent(httpsCookieAgentOptions); + options.httpsAgent = new HttpsCookieAgent( + httpsCookieAgentOptions + ); } if (this.auth_method === "mtls") { if (this.tlsCert !== null && this.tlsCert !== "") { - options.httpsAgent.options.cert = Buffer.from(this.tlsCert); + options.httpsAgent.options.cert = Buffer.from( + this.tlsCert + ); } if (this.tlsCa !== null && this.tlsCa !== "") { - options.httpsAgent.options.ca = Buffer.from(this.tlsCa); + options.httpsAgent.options.ca = Buffer.from( + this.tlsCa + ); } if (this.tlsKey !== null && this.tlsKey !== "") { - options.httpsAgent.options.key = Buffer.from(this.tlsKey); + options.httpsAgent.options.key = Buffer.from( + this.tlsKey + ); } } - log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`); + log.debug( + "monitor", + `[${this.name}] Axios Options: ${JSON.stringify( + options + )}` + ); log.debug("monitor", `[${this.name}] Axios Request`); // Make Request @@ -546,13 +676,22 @@ class Monitor extends BeanModel { let tlsInfoObject = checkCertificate(res); tlsInfo = await this.updateTlsInfo(tlsInfoObject); - if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) { - log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`); - await this.checkCertExpiryNotifications(tlsInfoObject); + if ( + !this.getIgnoreTls() && + this.isEnabledExpiryNotification() + ) { + log.debug( + "monitor", + `[${this.name}] call checkCertExpiryNotifications` + ); + await this.checkCertExpiryNotifications( + tlsInfoObject + ); } - } catch (e) { - if (e.message !== "No TLS certificate in response") { + if ( + e.message !== "No TLS certificate in response" + ) { log.error("monitor", "Caught error"); log.error("monitor", e.message); } @@ -560,17 +699,24 @@ class Monitor extends BeanModel { } if (process.env.TIMELOGGER === "1") { - log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms"); + log.debug( + "monitor", + "Cert Info Query Time: " + + (dayjs().valueOf() - certInfoStartTime) + + "ms" + ); } - if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === this.id) { + if ( + process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === + this.id + ) { log.info("monitor", res.data); } if (this.type === "http") { bean.status = UP; } else if (this.type === "keyword") { - let data = res.data; // Convert to string for object/array @@ -580,17 +726,30 @@ class Monitor extends BeanModel { let keywordFound = data.includes(this.keyword); if (keywordFound === !this.isInvertKeyword()) { - bean.msg += ", keyword " + (keywordFound ? "is" : "not") + " found"; + bean.msg += keywordFound + ? ", プロセスは正常に実行されています / keyword is found" + : ",プロセスが停止しました / keyword is not found"; + // bean.msg += + // ", keyword " + + // (keywordFound ? "is" : "not") + + // " found"; bean.status = UP; } else { - data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim(); + data = data + .replace(/<[^>]*>?|[\n\r]|\s+/gm, " ") + .trim(); if (data.length > 50) { data = data.substring(0, 47) + "..."; } - throw new Error(bean.msg + ", but keyword is " + - (keywordFound ? "present" : "not") + " in [" + data + "]"); + throw new Error( + bean.msg + + ",プロセスが停止しました / but keyword is " + + (keywordFound ? "present" : "not") + + " in [" + + data + + "]" + ); } - } else if (this.type === "json-query") { let data = res.data; @@ -607,31 +766,49 @@ class Monitor extends BeanModel { bean.msg += ", expected value is found"; bean.status = UP; } else { - throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]"); + throw new Error( + bean.msg + + ", but value is not equal to expected value, value was: [" + + result + + "]" + ); } } - } else if (this.type === "port") { bean.ping = await tcping(this.hostname, this.port); bean.msg = ""; bean.status = UP; - } else if (this.type === "ping") { bean.ping = await ping(this.hostname, this.packetSize); bean.msg = ""; bean.status = UP; - } else if (this.type === "push") { // Type: Push - log.debug("monitor", `[${this.name}] Checking monitor at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`); + } else if (this.type === "push") { + // Type: Push + log.debug( + "monitor", + `[${this.name}] Checking monitor at ${dayjs().format( + "YYYY-MM-DD HH:mm:ss.SSS" + )}` + ); const bufferTime = 1000; // 1s buffer to accommodate clock differences if (previousBeat) { - const msSinceLastBeat = dayjs.utc().valueOf() - dayjs.utc(previousBeat.time).valueOf(); + const msSinceLastBeat = + dayjs.utc().valueOf() - + dayjs.utc(previousBeat.time).valueOf(); - log.debug("monitor", `[${this.name}] msSinceLastBeat = ${msSinceLastBeat}`); + log.debug( + "monitor", + `[${this.name}] msSinceLastBeat = ${msSinceLastBeat}` + ); // If the previous beat was down or pending we use the regular // beatInterval/retryInterval in the setTimeout further below - if (previousBeat.status !== (this.isUpsideDown() ? DOWN : UP) || msSinceLastBeat > beatInterval * 1000 + bufferTime) { + if ( + previousBeat.status !== + (this.isUpsideDown() ? DOWN : UP) || + msSinceLastBeat > beatInterval * 1000 + bufferTime + ) { bean.duration = Math.round(msSinceLastBeat / 1000); throw new Error("No heartbeat in the time window"); } else { @@ -643,17 +820,23 @@ class Monitor extends BeanModel { } // No need to insert successful heartbeat for push type, so end here retries = 0; - log.debug("monitor", `[${this.name}] timeout = ${timeout}`); - this.heartbeatInterval = setTimeout(safeBeat, timeout); + log.debug( + "monitor", + `[${this.name}] timeout = ${timeout}` + ); + this.heartbeatInterval = setTimeout( + safeBeat, + timeout + ); return; } } else { bean.duration = beatInterval; throw new Error("No heartbeat in the time window"); } - } else if (this.type === "steam") { - const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/"; + const steamApiUrl = + "https://api.steampowered.com/IGameServersService/GetServerList/v1/"; const steamAPIKey = await setting("steamAPIKey"); const filter = `addr\\${this.hostname}:${this.port}`; @@ -664,33 +847,44 @@ class Monitor extends BeanModel { let res = await axios.get(steamApiUrl, { timeout: this.timeout * 1000, headers: { - "Accept": "*/*", + Accept: "*/*", }, httpsAgent: new https.Agent({ - maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) + maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) rejectUnauthorized: !this.getIgnoreTls(), - secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT, + secureOptions: + crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT, }), httpAgent: new http.Agent({ maxCachedSessions: 0, }), maxRedirects: this.maxredirects, validateStatus: (status) => { - return checkStatusCode(status, this.getAcceptedStatuscodes()); + return checkStatusCode( + status, + this.getAcceptedStatuscodes() + ); }, params: { filter: filter, key: steamAPIKey, - } + }, }); - if (res.data.response && res.data.response.servers && res.data.response.servers.length > 0) { + if ( + res.data.response && + res.data.response.servers && + res.data.response.servers.length > 0 + ) { bean.status = UP; bean.msg = res.data.response.servers[0].name; try { - bean.ping = await ping(this.hostname, this.packetSize); - } catch (_) { } + bean.ping = await ping( + this.hostname, + this.packetSize + ); + } catch (_) {} } else { throw new Error("Server not found on Steam"); } @@ -710,25 +904,32 @@ class Monitor extends BeanModel { throw new Error(e.message); } } else if (this.type === "docker") { - log.debug("monitor", `[${this.name}] Prepare Options for Axios`); + log.debug( + "monitor", + `[${this.name}] Prepare Options for Axios` + ); const options = { url: `/containers/${this.docker_container}/json`, timeout: this.interval * 1000 * 0.8, headers: { - "Accept": "*/*", + Accept: "*/*", }, httpsAgent: new https.Agent({ - maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) + maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) rejectUnauthorized: !this.getIgnoreTls(), - secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT, + secureOptions: + crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT, }), httpAgent: new http.Agent({ maxCachedSessions: 0, }), }; - const dockerHost = await R.load("docker_host", this.docker_host); + const dockerHost = await R.load( + "docker_host", + this.docker_host + ); if (!dockerHost) { throw new Error("Failed to load docker host config"); @@ -737,9 +938,14 @@ class Monitor extends BeanModel { if (dockerHost._dockerType === "socket") { options.socketPath = dockerHost._dockerDaemon; } else if (dockerHost._dockerType === "tcp") { - options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon); + options.baseURL = DockerHost.patchDockerURL( + dockerHost._dockerDaemon + ); options.httpsAgent = new https.Agent( - DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL) + DockerHost.getHttpsAgentOptions( + dockerHost._dockerType, + options.baseURL + ) ); } @@ -747,20 +953,30 @@ class Monitor extends BeanModel { let res = await axios.request(options); if (res.data.State.Running) { - if (res.data.State.Health && res.data.State.Health.Status !== "healthy") { + if ( + res.data.State.Health && + res.data.State.Health.Status !== "healthy" + ) { bean.status = PENDING; bean.msg = res.data.State.Health.Status; } else { bean.status = UP; - bean.msg = res.data.State.Health ? res.data.State.Health.Status : res.data.State.Status; + bean.msg = res.data.State.Health + ? res.data.State.Health.Status + : res.data.State.Status; } } else { - throw Error("Container State is " + res.data.State.Status); + throw Error( + "Container State is " + res.data.State.Status + ); } } else if (this.type === "sqlserver") { let startTime = dayjs().valueOf(); - await mssqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1"); + await mssqlQuery( + this.databaseConnectionString, + this.databaseQuery || "SELECT 1" + ); bean.msg = ""; bean.status = UP; @@ -777,29 +993,49 @@ class Monitor extends BeanModel { }; const response = await grpcQuery(options); bean.ping = dayjs().valueOf() - startTime; - log.debug("monitor:", `gRPC response: ${JSON.stringify(response)}`); + log.debug( + "monitor:", + `gRPC response: ${JSON.stringify(response)}` + ); let responseData = response.data; if (responseData.length > 50) { - responseData = responseData.toString().substring(0, 47) + "..."; + responseData = + responseData.toString().substring(0, 47) + "..."; } if (response.code !== 1) { bean.status = DOWN; bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`; } else { - let keywordFound = response.data.toString().includes(this.keyword); + let keywordFound = response.data + .toString() + .includes(this.keyword); if (keywordFound === !this.isInvertKeyword()) { bean.status = UP; - bean.msg = `${responseData}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`; + bean.msg = `${responseData}, keyword [${ + this.keyword + }] ${keywordFound ? "is" : "not"} found`; } else { - log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response.data} + "]"`); + log.debug( + "monitor:", + `GRPC response [${ + response.data + }] + ", but keyword [${this.keyword}] is ${ + keywordFound ? "present" : "not" + } in [" + ${response.data} + "]"` + ); bean.status = DOWN; - bean.msg = `, but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${responseData} + "]`; + bean.msg = `, but keyword [${this.keyword}] is ${ + keywordFound ? "present" : "not" + } in [" + ${responseData} + "]`; } } } else if (this.type === "postgres") { let startTime = dayjs().valueOf(); - await postgresQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1"); + await postgresQuery( + this.databaseConnectionString, + this.databaseQuery || "SELECT 1" + ); bean.msg = ""; bean.status = UP; @@ -811,7 +1047,11 @@ class Monitor extends BeanModel { // TODO: rename `radius_password` to `password` later for general use let mysqlPassword = this.radiusPassword; - bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1", mysqlPassword); + bean.msg = await mysqlQuery( + this.databaseConnectionString, + this.databaseQuery || "SELECT 1", + mysqlPassword + ); bean.status = UP; bean.ping = dayjs().valueOf() - startTime; } else if (this.type === "mongodb") { @@ -822,7 +1062,6 @@ class Monitor extends BeanModel { bean.msg = ""; bean.status = UP; bean.ping = dayjs().valueOf() - startTime; - } else if (this.type === "radius") { let startTime = dayjs().valueOf(); @@ -844,7 +1083,7 @@ class Monitor extends BeanModel { this.radiusCallingStationId, this.radiusSecret, port, - this.interval * 1000 * 0.4, + this.interval * 1000 * 0.4 ); bean.msg = resp.code; @@ -853,18 +1092,23 @@ class Monitor extends BeanModel { } else if (this.type === "redis") { let startTime = dayjs().valueOf(); - bean.msg = await redisPingAsync(this.databaseConnectionString); + bean.msg = await redisPingAsync( + this.databaseConnectionString + ); bean.status = UP; bean.ping = dayjs().valueOf() - startTime; - } else if (this.type in UptimeKumaServer.monitorTypeList) { let startTime = dayjs().valueOf(); - const monitorType = UptimeKumaServer.monitorTypeList[this.type]; - await monitorType.check(this, bean, UptimeKumaServer.getInstance()); + const monitorType = + UptimeKumaServer.monitorTypeList[this.type]; + await monitorType.check( + this, + bean, + UptimeKumaServer.getInstance() + ); if (!bean.ping) { bean.ping = dayjs().valueOf() - startTime; } - } else if (this.type === "kafka-producer") { let startTime = dayjs().valueOf(); @@ -873,16 +1117,16 @@ class Monitor extends BeanModel { this.kafkaProducerTopic, this.kafkaProducerMessage, { - allowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation, + allowAutoTopicCreation: + this.kafkaProducerAllowAutoTopicCreation, ssl: this.kafkaProducerSsl, clientId: `Uptime-Kuma/${version}`, interval: this.interval, }, - JSON.parse(this.kafkaProducerSaslOptions), + JSON.parse(this.kafkaProducerSaslOptions) ); bean.status = UP; bean.ping = dayjs().valueOf() - startTime; - } else { throw new Error("Unknown Monitor Type"); } @@ -896,21 +1140,18 @@ class Monitor extends BeanModel { } retries = 0; - } catch (error) { - if (error?.name === "CanceledError") { - bean.msg = `timeout by AbortSignal (${this.timeout}s)`; + bean.msg = `サーバーにアクセスできません / timeout by AbortSignal (${this.timeout}s)`; } else { - bean.msg = error.message; + bean.msg = `サーバーにアクセスできません / ${error.message}`; } // If UP come in here, it must be upside down mode // Just reset the retries if (this.isUpsideDown() && bean.status === UP) { retries = 0; - - } else if ((this.maxretries > 0) && (retries < this.maxretries)) { + } else if (this.maxretries > 0 && retries < this.maxretries) { retries++; bean.status = PENDING; } else { @@ -922,18 +1163,31 @@ class Monitor extends BeanModel { bean.retries = retries; log.debug("monitor", `[${this.name}] Check isImportant`); - let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status); + let isImportant = Monitor.isImportantBeat( + isFirstBeat, + previousBeat?.status, + bean.status + ); // Mark as important if status changed, ignore pending pings, // Don't notify if disrupted changes to up if (isImportant) { bean.important = true; - if (Monitor.isImportantForNotification(isFirstBeat, previousBeat?.status, bean.status)) { + if ( + Monitor.isImportantForNotification( + isFirstBeat, + previousBeat?.status, + bean.status + ) + ) { log.debug("monitor", `[${this.name}] sendNotification`); await Monitor.sendNotification(isFirstBeat, this, bean); } else { - log.debug("monitor", `[${this.name}] will not sendNotification because it is (or was) under maintenance`); + log.debug( + "monitor", + `[${this.name}] will not sendNotification because it is (or was) under maintenance` + ); } // Reset down count @@ -943,8 +1197,9 @@ class Monitor extends BeanModel { log.debug("monitor", `[${this.name}] apicache clear`); apicache.clear(); - UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id); - + UptimeKumaServer.getInstance().sendMaintenanceListByUserID( + this.user_id + ); } else { bean.important = false; @@ -952,7 +1207,10 @@ class Monitor extends BeanModel { ++bean.downCount; if (bean.downCount >= this.resendInterval) { // Send notification again, because we are still DOWN - log.debug("monitor", `[${this.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`); + log.debug( + "monitor", + `[${this.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}` + ); await Monitor.sendNotification(isFirstBeat, this, bean); // Reset down count @@ -962,21 +1220,38 @@ class Monitor extends BeanModel { } if (bean.status === UP) { - log.debug("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`); + log.debug( + "monitor", + `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}` + ); } else if (bean.status === PENDING) { if (this.retryInterval > 0) { beatInterval = this.retryInterval; } - log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`); + log.warn( + "monitor", + `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}` + ); } else if (bean.status === MAINTENANCE) { - log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`); + log.warn( + "monitor", + `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}` + ); } else { - log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`); + log.warn( + "monitor", + `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}` + ); } // Calculate uptime - let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(this.id); - let endTimeDayjs = await uptimeCalculator.update(bean.status, parseFloat(bean.ping)); + let uptimeCalculator = await UptimeCalculator.getUptimeCalculator( + this.id + ); + let endTimeDayjs = await uptimeCalculator.update( + bean.status, + parseFloat(bean.ping) + ); bean.end_time = R.isoDateTimeMillis(endTimeDayjs); // Send to frontend @@ -993,21 +1268,32 @@ class Monitor extends BeanModel { previousBeat = bean; - if (! this.isStop) { - log.debug("monitor", `[${this.name}] SetTimeout for next check.`); + if (!this.isStop) { + log.debug( + "monitor", + `[${this.name}] SetTimeout for next check.` + ); let intervalRemainingMs = Math.max( 1, beatInterval * 1000 - dayjs().diff(dayjs.utc(bean.time)) ); - log.debug("monitor", `[${this.name}] Next heartbeat in: ${intervalRemainingMs}ms`); + log.debug( + "monitor", + `[${this.name}] Next heartbeat in: ${intervalRemainingMs}ms` + ); - this.heartbeatInterval = setTimeout(safeBeat, intervalRemainingMs); + this.heartbeatInterval = setTimeout( + safeBeat, + intervalRemainingMs + ); } else { - log.info("monitor", `[${this.name}] isStop = true, no next check.`); + log.info( + "monitor", + `[${this.name}] isStop = true, no next check.` + ); } - }; /** @@ -1020,11 +1306,17 @@ class Monitor extends BeanModel { } catch (e) { console.trace(e); UptimeKumaServer.errorLog(e, false); - log.error("monitor", "Please report to https://github.com/louislam/uptime-kuma/issues"); + log.error( + "monitor", + "Please report to https://github.com/louislam/uptime-kuma/issues" + ); - if (! this.isStop) { + if (!this.isStop) { log.info("monitor", "Try to restart the monitor"); - this.heartbeatInterval = setTimeout(safeBeat, this.interval * 1000); + this.heartbeatInterval = setTimeout( + safeBeat, + this.interval * 1000 + ); } } }; @@ -1056,7 +1348,9 @@ class Monitor extends BeanModel { username: this.basic_auth_user, password: this.basic_auth_pass, domain: this.authDomain, - workstation: this.authWorkstation ? this.authWorkstation : undefined + workstation: this.authWorkstation + ? this.authWorkstation + : undefined, }); } else { res = await axios.request(options); @@ -1064,32 +1358,47 @@ class Monitor extends BeanModel { return res; } catch (error) { - /** * Make a single attempt to obtain an new access token in the event that * the recent api request failed for authentication purposes */ - if (this.auth_method === "oauth2-cc" && error.response.status === 401 && !finalCall) { - this.oauthAccessToken = await this.makeOidcTokenClientCredentialsRequest(); + if ( + this.auth_method === "oauth2-cc" && + error.response.status === 401 && + !finalCall + ) { + this.oauthAccessToken = + await this.makeOidcTokenClientCredentialsRequest(); let oauth2AuthHeader = { - "Authorization": this.oauthAccessToken.token_type + " " + this.oauthAccessToken.access_token, - }; - options.headers = { ...(options.headers), - ...(oauth2AuthHeader) + Authorization: + this.oauthAccessToken.token_type + + " " + + this.oauthAccessToken.access_token, }; + options.headers = { ...options.headers, ...oauth2AuthHeader }; return this.makeAxiosRequest(options, true); } // Fix #2253 // Read more: https://stackoverflow.com/questions/1759956/curl-error-18-transfer-closed-with-outstanding-read-data-remaining - if (!finalCall && typeof error.message === "string" && error.message.includes("maxContentLength size of -1 exceeded")) { + if ( + !finalCall && + typeof error.message === "string" && + error.message.includes("maxContentLength size of -1 exceeded") + ) { log.debug("monitor", "makeAxiosRequest with gzip"); options.headers["Accept-Encoding"] = "gzip, deflate"; return this.makeAxiosRequest(options, true); } else { - if (typeof error.message === "string" && error.message.includes("maxContentLength size of -1 exceeded")) { - error.message = "response timeout: incomplete response within a interval"; + if ( + typeof error.message === "string" && + error.message.includes( + "maxContentLength size of -1 exceeded" + ) + ) { + error.message = + "response timeout: incomplete response within a interval"; } throw error; } @@ -1148,37 +1457,51 @@ class Monitor extends BeanModel { * @returns {Promise} Updated certificate */ async updateTlsInfo(checkCertificateResult) { - let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ - this.id, - ]); + let tlsInfoBean = await R.findOne( + "monitor_tls_info", + "monitor_id = ?", + [this.id] + ); if (tlsInfoBean == null) { tlsInfoBean = R.dispense("monitor_tls_info"); tlsInfoBean.monitor_id = this.id; } else { - // Clear sent history if the cert changed. try { let oldCertInfo = JSON.parse(tlsInfoBean.info_json); - let isValidObjects = oldCertInfo && oldCertInfo.certInfo && checkCertificateResult && checkCertificateResult.certInfo; + let isValidObjects = + oldCertInfo && + oldCertInfo.certInfo && + checkCertificateResult && + checkCertificateResult.certInfo; if (isValidObjects) { - if (oldCertInfo.certInfo.fingerprint256 !== checkCertificateResult.certInfo.fingerprint256) { + if ( + oldCertInfo.certInfo.fingerprint256 !== + checkCertificateResult.certInfo.fingerprint256 + ) { log.debug("monitor", "Resetting sent_history"); - await R.exec("DELETE FROM notification_sent_history WHERE type = 'certificate' AND monitor_id = ?", [ - this.id - ]); + await R.exec( + "DELETE FROM notification_sent_history WHERE type = 'certificate' AND monitor_id = ?", + [this.id] + ); } else { log.debug("monitor", "No need to reset sent_history"); - log.debug("monitor", oldCertInfo.certInfo.fingerprint256); - log.debug("monitor", checkCertificateResult.certInfo.fingerprint256); + log.debug( + "monitor", + oldCertInfo.certInfo.fingerprint256 + ); + log.debug( + "monitor", + checkCertificateResult.certInfo.fingerprint256 + ); } } else { log.debug("monitor", "Not valid object"); } - } catch (e) { } - + } catch (e) {} } tlsInfoBean.info_json = JSON.stringify(checkCertificateResult); @@ -1196,12 +1519,18 @@ class Monitor extends BeanModel { */ static async sendStats(io, monitorID, userID) { const hasClients = getTotalClientInRoom(io, userID) > 0; - let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID); + let uptimeCalculator = await UptimeCalculator.getUptimeCalculator( + monitorID + ); if (hasClients) { // Send 24 hour average ping let data24h = await uptimeCalculator.get24Hour(); - io.to(userID).emit("avgPing", monitorID, (data24h.avgPing) ? Number(data24h.avgPing.toFixed(2)) : null); + io.to(userID).emit( + "avgPing", + monitorID, + data24h.avgPing ? Number(data24h.avgPing.toFixed(2)) : null + ); // Send 24 hour uptime io.to(userID).emit("uptime", monitorID, 24, data24h.uptime); @@ -1217,7 +1546,10 @@ class Monitor extends BeanModel { // Send Cert Info await Monitor.sendCertInfo(io, monitorID, userID); } else { - log.debug("monitor", "No clients in the room, no need to send stats"); + log.debug( + "monitor", + "No clients in the room, no need to send stats" + ); } } @@ -1260,14 +1592,18 @@ class Monitor extends BeanModel { // * MAINTENANCE -> DOWN = important // * DOWN -> MAINTENANCE = important // * UP -> MAINTENANCE = important - return isFirstBeat || - (previousBeatStatus === DOWN && currentBeatStatus === MAINTENANCE) || + return ( + isFirstBeat || + (previousBeatStatus === DOWN && + currentBeatStatus === MAINTENANCE) || (previousBeatStatus === UP && currentBeatStatus === MAINTENANCE) || - (previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN) || + (previousBeatStatus === MAINTENANCE && + currentBeatStatus === DOWN) || (previousBeatStatus === MAINTENANCE && currentBeatStatus === UP) || (previousBeatStatus === UP && currentBeatStatus === DOWN) || (previousBeatStatus === DOWN && currentBeatStatus === UP) || - (previousBeatStatus === PENDING && currentBeatStatus === DOWN); + (previousBeatStatus === PENDING && currentBeatStatus === DOWN) + ); } /** @@ -1277,7 +1613,11 @@ class Monitor extends BeanModel { * @param {const} currentBeatStatus Status of the current beat * @returns {boolean} True if is an important beat else false */ - static isImportantForNotification(isFirstBeat, previousBeatStatus, currentBeatStatus) { + static isImportantForNotification( + isFirstBeat, + previousBeatStatus, + currentBeatStatus + ) { // * ? -> ANY STATUS = important [isFirstBeat] // UP -> PENDING = not important // * UP -> DOWN = important @@ -1293,11 +1633,14 @@ class Monitor extends BeanModel { // * MAINTENANCE -> DOWN = important // DOWN -> MAINTENANCE = not important // UP -> MAINTENANCE = not important - return isFirstBeat || - (previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN) || + return ( + isFirstBeat || + (previousBeatStatus === MAINTENANCE && + currentBeatStatus === DOWN) || (previousBeatStatus === UP && currentBeatStatus === DOWN) || (previousBeatStatus === DOWN && currentBeatStatus === UP) || - (previousBeatStatus === PENDING && currentBeatStatus === DOWN); + (previousBeatStatus === PENDING && currentBeatStatus === DOWN) + ); } /** @@ -1330,13 +1673,26 @@ class Monitor extends BeanModel { } // Also provide the time in server timezone - heartbeatJSON["timezone"] = await UptimeKumaServer.getInstance().getTimezone(); - heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset(); - heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT); - - await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON); + heartbeatJSON["timezone"] = + await UptimeKumaServer.getInstance().getTimezone(); + heartbeatJSON["timezoneOffset"] = + UptimeKumaServer.getInstance().getTimezoneOffset(); + heartbeatJSON["localDateTime"] = dayjs + .utc(heartbeatJSON["time"]) + .tz(heartbeatJSON["timezone"]) + .format(SQL_DATETIME_FORMAT); + + await Notification.send( + JSON.parse(notification.config), + msg, + await monitor.toJSON(false), + heartbeatJSON + ); } catch (e) { - log.error("monitor", "Cannot send notification to " + notification.name); + log.error( + "monitor", + "Cannot send notification to " + notification.name + ); log.error("monitor", e); } } @@ -1349,9 +1705,10 @@ class Monitor extends BeanModel { * @returns {Promise[]>} List of notifications */ static async getNotificationList(monitor) { - let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [ - monitor.id, - ]); + let notificationList = await R.getAll( + "SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", + [monitor.id] + ); return notificationList; } @@ -1361,20 +1718,27 @@ class Monitor extends BeanModel { * @returns {void} */ async checkCertExpiryNotifications(tlsInfoObject) { - if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) { + if ( + tlsInfoObject && + tlsInfoObject.certInfo && + tlsInfoObject.certInfo.daysRemaining + ) { const notificationList = await Monitor.getNotificationList(this); - if (! notificationList.length > 0) { + if (!notificationList.length > 0) { // fail fast. If no notification is set, all the following checks can be skipped. - log.debug("monitor", "No notification, no need to send cert notification"); + log.debug( + "monitor", + "No notification, no need to send cert notification" + ); return; } let notifyDays = await setting("tlsExpiryNotifyDays"); if (notifyDays == null || !Array.isArray(notifyDays)) { // Reset Default - setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general"); - notifyDays = [ 7, 14, 21 ]; + setSetting("tlsExpiryNotifyDays", [7, 14, 21], "general"); + notifyDays = [7, 14, 21]; } if (Array.isArray(notifyDays)) { @@ -1383,13 +1747,28 @@ class Monitor extends BeanModel { while (certInfo) { let subjectCN = certInfo.subject["CN"]; if (rootCertificates.has(certInfo.fingerprint256)) { - log.debug("monitor", `Known root cert: ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`); + log.debug( + "monitor", + `Known root cert: ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.` + ); break; } else if (certInfo.daysRemaining > targetDays) { - log.debug("monitor", `No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`); + log.debug( + "monitor", + `No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.` + ); } else { - log.debug("monitor", `call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`); - await this.sendCertNotificationByTargetDays(subjectCN, certInfo.certType, certInfo.daysRemaining, targetDays, notificationList); + log.debug( + "monitor", + `call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.` + ); + await this.sendCertNotificationByTargetDays( + subjectCN, + certInfo.certType, + certInfo.daysRemaining, + targetDays, + notificationList + ); } certInfo = certInfo.issuerCertificate; } @@ -1408,13 +1787,17 @@ class Monitor extends BeanModel { * @param {LooseObject[]} notificationList List of notification providers * @returns {Promise} */ - async sendCertNotificationByTargetDays(certCN, certType, daysRemaining, targetDays, notificationList) { - - let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?", [ - "certificate", - this.id, - targetDays, - ]); + async sendCertNotificationByTargetDays( + certCN, + certType, + daysRemaining, + targetDays, + notificationList + ) { + let row = await R.getRow( + "SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?", + ["certificate", this.id, targetDays] + ); // Sent already, no need to send again if (row) { @@ -1428,20 +1811,25 @@ class Monitor extends BeanModel { for (let notification of notificationList) { try { log.debug("monitor", "Sending to " + notification.name); - await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] ${certType} certificate ${certCN} will be expired in ${daysRemaining} days`); + await Notification.send( + JSON.parse(notification.config), + `[${this.name}][${this.url}] ${certType} certificate ${certCN} will be expired in ${daysRemaining} days` + ); sent = true; } catch (e) { - log.error("monitor", "Cannot send cert notification to " + notification.name); + log.error( + "monitor", + "Cannot send cert notification to " + notification.name + ); log.error("monitor", e); } } if (sent) { - await R.exec("INSERT INTO notification_sent_history (type, monitor_id, days) VALUES(?, ?, ?)", [ - "certificate", - this.id, - targetDays, - ]); + await R.exec( + "INSERT INTO notification_sent_history (type, monitor_id, days) VALUES(?, ?, ?)", + ["certificate", this.id, targetDays] + ); } } @@ -1451,9 +1839,11 @@ class Monitor extends BeanModel { * @returns {Promise>} Previous heartbeat */ static async getPreviousHeartbeat(monitorID) { - return await R.findOne("heartbeat", " id = (select MAX(id) from heartbeat where monitor_id = ?)", [ - monitorID - ]); + return await R.findOne( + "heartbeat", + " id = (select MAX(id) from heartbeat where monitor_id = ?)", + [monitorID] + ); } /** @@ -1462,14 +1852,20 @@ class Monitor extends BeanModel { * @returns {Promise} Is the monitor under maintenance */ static async isUnderMaintenance(monitorID) { - const maintenanceIDList = await R.getCol(` + const maintenanceIDList = await R.getCol( + ` SELECT maintenance_id FROM monitor_maintenance WHERE monitor_id = ? - `, [ monitorID ]); + `, + [monitorID] + ); for (const maintenanceID of maintenanceIDList) { - const maintenance = await UptimeKumaServer.getInstance().getMaintenance(maintenanceID); - if (maintenance && await maintenance.isUnderMaintenance()) { + const maintenance = + await UptimeKumaServer.getInstance().getMaintenance( + maintenanceID + ); + if (maintenance && (await maintenance.isUnderMaintenance())) { return true; } } @@ -1489,10 +1885,14 @@ class Monitor extends BeanModel { */ validate() { if (this.interval > MAX_INTERVAL_SECOND) { - throw new Error(`Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds`); + throw new Error( + `Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds` + ); } if (this.interval < MIN_INTERVAL_SECOND) { - throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`); + throw new Error( + `Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds` + ); } } @@ -1502,14 +1902,15 @@ class Monitor extends BeanModel { * @returns {Promise>} Parent */ static async getParent(monitorID) { - return await R.getRow(` + return await R.getRow( + ` SELECT parent.* FROM monitor parent LEFT JOIN monitor child ON child.parent = parent.id WHERE child.id = ? - `, [ - monitorID, - ]); + `, + [monitorID] + ); } /** @@ -1518,12 +1919,13 @@ class Monitor extends BeanModel { * @returns {Promise>} Children */ static async getChildren(monitorID) { - return await R.getAll(` + return await R.getAll( + ` SELECT * FROM monitor WHERE parent = ? - `, [ - monitorID, - ]); + `, + [monitorID] + ); } /** @@ -1562,7 +1964,9 @@ class Monitor extends BeanModel { for (const child of childs) { childrenIDs.push(child.id); - childrenIDs = childrenIDs.concat(await Monitor.getAllChildrenIDs(child.id)); + childrenIDs = childrenIDs.concat( + await Monitor.getAllChildrenIDs(child.id) + ); } return childrenIDs; @@ -1574,9 +1978,10 @@ class Monitor extends BeanModel { * @returns {Promise} */ static async unlinkAllChildren(groupID) { - return await R.exec("UPDATE `monitor` SET parent = ? WHERE parent = ? ", [ - null, groupID - ]); + return await R.exec( + "UPDATE `monitor` SET parent = ? WHERE parent = ? ", + [null, groupID] + ); } /** @@ -1600,17 +2005,35 @@ class Monitor extends BeanModel { * @returns {Promise} OAuthProvider client */ async makeOidcTokenClientCredentialsRequest() { - log.debug("monitor", `[${this.name}] The oauth access-token undefined or expired. Requesting a new token`); - const oAuthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_auth_method); + log.debug( + "monitor", + `[${this.name}] The oauth access-token undefined or expired. Requesting a new token` + ); + const oAuthAccessToken = await getOidcTokenClientCredentials( + this.oauth_token_url, + this.oauth_client_id, + this.oauth_client_secret, + this.oauth_scopes, + this.oauth_auth_method + ); if (this.oauthAccessToken?.expires_at) { - log.debug("monitor", `[${this.name}] Obtained oauth access-token. Expires at ${new Date(this.oauthAccessToken?.expires_at * 1000)}`); + log.debug( + "monitor", + `[${ + this.name + }] Obtained oauth access-token. Expires at ${new Date( + this.oauthAccessToken?.expires_at * 1000 + )}` + ); } else { - log.debug("monitor", `[${this.name}] Obtained oauth access-token. Time until expiry was not provided`); + log.debug( + "monitor", + `[${this.name}] Obtained oauth access-token. Time until expiry was not provided` + ); } return oAuthAccessToken; } - } module.exports = Monitor; diff --git a/server/routers/api-router.js b/server/routers/api-router.js index b63b5123b8..cb6eb5762d 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -11,14 +11,23 @@ const { R } = require("redbean-node"); const apicache = require("../modules/apicache"); const Monitor = require("../model/monitor"); const dayjs = require("dayjs"); -const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log, badgeConstants } = require("../../src/util"); +const { + UP, + MAINTENANCE, + DOWN, + PENDING, + flipStatus, + log, + badgeConstants, + genSecret, +} = require("../../src/util"); const StatusPage = require("../model/status_page"); const { UptimeKumaServer } = require("../uptime-kuma-server"); const { makeBadge } = require("badge-maker"); const { Prometheus } = require("../prometheus"); const Database = require("../database"); const { UptimeCalculator } = require("../uptime-calculator"); - +const axios = require("axios"); let router = express.Router(); let cache = apicache.middleware; @@ -28,7 +37,7 @@ let io = server.io; router.get("/api/entry-page", async (request, response) => { allowDevAllOrigin(response); - let result = { }; + let result = {}; let hostname = request.hostname; if ((await setting("trustProxy")) && request.headers["x-forwarded-host"]) { hostname = request.headers["x-forwarded-host"]; @@ -44,24 +53,201 @@ router.get("/api/entry-page", async (request, response) => { response.json(result); }); -router.get("/api/push/:pushToken", async (request, response) => { +router.post("/api/monitor/:name/heartbeats", async (request, response) => { + allowAllOrigin(response); + const name = request.params.name; + const monitor = await R.findOne("monitor", "description = ? ", [name]); + if (!monitor) { + response.status(404).json({ + ok: false, + msg: "Monitor not found", + }); + return; + } + const oneHourAgo = dayjs().subtract(1, "hour").format(); + log.info("monitor", `onehourago ${oneHourAgo}`); + const heartbeats = await R.find( + "heartbeat", + "monitor_id = ? AND time >= ? ORDER BY time DESC", + [monitor.id, oneHourAgo] + ); + + response.json({ + ok: true, + data: heartbeats, + }); +}); +const pushTokenLength = 32; + +router.post("/api/monitor/add", async (request, response) => { + allowAllOrigin(response); + const userId = 1; + const { name } = request.body; + const refinedName = name.replace(/[^\w\s]/gi, "").toLowerCase(); + const restartUrl = `http://anpr_client_${refinedName}:5000/restart`; + const restartInterval = 100; + const monitor = { + type: "http", + name: "", + parent: null, + url: "https://", + restartUrl: restartUrl, + restartInterval: restartInterval, + method: "GET", + interval: 60, + retryInterval: 60, + resendInterval: 0, + maxretries: 0, + timeout: 48, + ignoreTls: false, + upsideDown: false, + packetSize: 56, + expiryNotification: false, + maxredirects: 10, + accepted_statuscodes: ["200-299"], + dns_resolve_type: "A", + dns_resolve_server: "1.1.1.1", + docker_container: "", + docker_host: null, + proxyId: null, + mqttUsername: "", + mqttPassword: "", + mqttTopic: "", + mqttSuccessMessage: "", + mqttCheckType: "keyword", + authMethod: null, + oauth_auth_method: "client_secret_basic", + httpBodyEncoding: "json", + kafkaProducerBrokers: [], + kafkaProducerSaslOptions: { + mechanism: "None", + }, + kafkaProducerSsl: false, + kafkaProducerAllowAutoTopicCreation: false, + gamedigGivenPortOnly: true, + remote_browser: null, + }; + + const bean = R.dispense("monitor"); + + monitor.accepted_statuscodes_json = JSON.stringify( + monitor.accepted_statuscodes + ); + delete monitor.accepted_statuscodes; + + monitor.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers); + monitor.kafkaProducerSaslOptions = JSON.stringify( + monitor.kafkaProducerSaslOptions + ); + + const pushToken = genSecret(pushTokenLength); + + bean.import(monitor); + bean.name = name; + bean.restartUrl = restartUrl; + bean.userId = userId; + bean.restartInterval = restartInterval; + bean.type = "push"; + bean.retryInterval = 50; + bean.interval = 50; + bean.weight = 2000; + bean.pushToken = pushToken; + bean.method = "GET"; + + bean.validate(); + + await R.store(bean); + + const pushUrl = `http://${request.headers.host}/api/push/${pushToken}?status=up&msg=OK&ping=`; + + if (bean.active !== false) { + const list = await server.getMonitorJSONList(userId); + io.to(userId).emit("monitorList", list); + let monitor = await R.findOne("monitor", " id = ? ", [bean.id]); + + if (monitor.id in server.monitorList) { + server.monitorList[monitor.id].stop(); + } + + server.monitorList[monitor.id] = monitor; + monitor.start(io); + } + + response.json({ + ok: true, + data: { + monitor, + restartInterval, + restartUrl, + pushUrl, + }, + }); +}); + +router.post("/api/monitor/:name/restart", async (request, response) => { + allowAllOrigin(response); + const monitorName = request.params.name; + const monitor = await R.findOne("monitor", " description = ? OR id = ? ", [ + monitorName, + monitorName, + ]); + log.debug( + "router", + `/api/monitor/${monitorName}/restart called at ${dayjs().format( + "YYYY-MM-DD HH:mm:ss.SSS" + )}` + ); + if (!monitor) { + response.status(404).json({ + ok: false, + msg: "Monitor not found", + }); + return; + } + const restartUrl = monitor.restartUrl; + if (!restartUrl) { + response.status(404).json({ + ok: false, + msg: "Restart URL not found", + }); + return; + } try { + await axios.get(restartUrl); + response.json({ + ok: true, + }); + } catch (e) { + log.error("router", e); + response.status(503).json({ + ok: false, + msg: "Service to restart is not available", + }); + return; + } +}); +router.get("/api/push/:pushToken", async (request, response) => { + try { let pushToken = request.params.pushToken; let msg = request.query.msg || "OK"; let ping = parseFloat(request.query.ping) || null; let statusString = request.query.status || "up"; - let status = (statusString === "up") ? UP : DOWN; + let status = statusString === "up" ? UP : DOWN; - let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [ - pushToken - ]); + let monitor = await R.findOne( + "monitor", + " push_token = ? AND active = 1 ", + [pushToken] + ); - if (! monitor) { + if (!monitor) { throw new Error("Monitor not found or not active."); } - const previousHeartbeat = await Monitor.getPreviousHeartbeat(monitor.id); + const previousHeartbeat = await Monitor.getPreviousHeartbeat( + monitor.id + ); let isFirstBeat = true; @@ -74,28 +260,55 @@ router.get("/api/push/:pushToken", async (request, response) => { if (previousHeartbeat) { isFirstBeat = false; - bean.duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second"); + bean.duration = dayjs(bean.time).diff( + dayjs(previousHeartbeat.time), + "second" + ); } if (await Monitor.isUnderMaintenance(monitor.id)) { msg = "Monitor under maintenance"; bean.status = MAINTENANCE; } else { - determineStatus(status, previousHeartbeat, monitor.maxretries, monitor.isUpsideDown(), bean); + determineStatus( + status, + previousHeartbeat, + monitor.maxretries, + monitor.isUpsideDown(), + bean + ); } // Calculate uptime - let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitor.id); - let endTimeDayjs = await uptimeCalculator.update(bean.status, parseFloat(bean.ping)); + let uptimeCalculator = await UptimeCalculator.getUptimeCalculator( + monitor.id + ); + let endTimeDayjs = await uptimeCalculator.update( + bean.status, + parseFloat(bean.ping) + ); bean.end_time = R.isoDateTimeMillis(endTimeDayjs); - log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`); + log.debug( + "router", + `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}` + ); log.debug("router", "PreviousStatus: " + previousHeartbeat?.status); log.debug("router", "Current Status: " + bean.status); - bean.important = Monitor.isImportantBeat(isFirstBeat, previousHeartbeat?.status, status); + bean.important = Monitor.isImportantBeat( + isFirstBeat, + previousHeartbeat?.status, + status + ); - if (Monitor.isImportantForNotification(isFirstBeat, previousHeartbeat?.status, status)) { + if ( + Monitor.isImportantForNotification( + isFirstBeat, + previousHeartbeat?.status, + status + ) + ) { // Reset down count bean.downCount = 0; @@ -106,7 +319,10 @@ router.get("/api/push/:pushToken", async (request, response) => { ++bean.downCount; if (bean.downCount >= this.resendInterval) { // Send notification again, because we are still DOWN - log.debug("monitor", `[${this.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`); + log.debug( + "monitor", + `[${this.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}` + ); await Monitor.sendNotification(isFirstBeat, this, bean); // Reset down count @@ -128,248 +344,295 @@ router.get("/api/push/:pushToken", async (request, response) => { } catch (e) { response.status(404).json({ ok: false, - msg: e.message + msg: e.message, }); } }); -router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => { - allowAllOrigin(response); - - const { - label, - upLabel = "Up", - downLabel = "Down", - pendingLabel = "Pending", - maintenanceLabel = "Maintenance", - upColor = badgeConstants.defaultUpColor, - downColor = badgeConstants.defaultDownColor, - pendingColor = badgeConstants.defaultPendingColor, - maintenanceColor = badgeConstants.defaultMaintenanceColor, - style = badgeConstants.defaultStyle, - value, // for demo purpose only - } = request.query; - - try { - const requestedMonitorId = parseInt(request.params.id, 10); - const overrideValue = value !== undefined ? parseInt(value) : undefined; - - let publicMonitor = await R.getRow(` +router.get( + "/api/badge/:id/status", + cache("5 minutes"), + async (request, response) => { + allowAllOrigin(response); + + const { + label, + upLabel = "Up", + downLabel = "Down", + pendingLabel = "Pending", + maintenanceLabel = "Maintenance", + upColor = badgeConstants.defaultUpColor, + downColor = badgeConstants.defaultDownColor, + pendingColor = badgeConstants.defaultPendingColor, + maintenanceColor = badgeConstants.defaultMaintenanceColor, + style = badgeConstants.defaultStyle, + value, // for demo purpose only + } = request.query; + + try { + const requestedMonitorId = parseInt(request.params.id, 10); + const overrideValue = + value !== undefined ? parseInt(value) : undefined; + + let publicMonitor = await R.getRow( + ` SELECT monitor_group.monitor_id FROM monitor_group, \`group\` WHERE monitor_group.group_id = \`group\`.id AND monitor_group.monitor_id = ? AND public = 1 `, - [ requestedMonitorId ] - ); - - const badgeValues = { style }; + [requestedMonitorId] + ); - if (!publicMonitor) { - // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant + const badgeValues = { style }; - badgeValues.message = "N/A"; - badgeValues.color = badgeConstants.naColor; - } else { - const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId); - const state = overrideValue !== undefined ? overrideValue : heartbeat.status; + if (!publicMonitor) { + // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant - if (label === undefined) { - badgeValues.label = "Status"; + badgeValues.message = "N/A"; + badgeValues.color = badgeConstants.naColor; } else { - badgeValues.label = label; - } - switch (state) { - case DOWN: - badgeValues.color = downColor; - badgeValues.message = downLabel; - break; - case UP: - badgeValues.color = upColor; - badgeValues.message = upLabel; - break; - case PENDING: - badgeValues.color = pendingColor; - badgeValues.message = pendingLabel; - break; - case MAINTENANCE: - badgeValues.color = maintenanceColor; - badgeValues.message = maintenanceLabel; - break; - default: - badgeValues.color = badgeConstants.naColor; - badgeValues.message = "N/A"; + const heartbeat = await Monitor.getPreviousHeartbeat( + requestedMonitorId + ); + const state = + overrideValue !== undefined + ? overrideValue + : heartbeat.status; + + if (label === undefined) { + badgeValues.label = "Status"; + } else { + badgeValues.label = label; + } + switch (state) { + case DOWN: + badgeValues.color = downColor; + badgeValues.message = downLabel; + break; + case UP: + badgeValues.color = upColor; + badgeValues.message = upLabel; + break; + case PENDING: + badgeValues.color = pendingColor; + badgeValues.message = pendingLabel; + break; + case MAINTENANCE: + badgeValues.color = maintenanceColor; + badgeValues.message = maintenanceLabel; + break; + default: + badgeValues.color = badgeConstants.naColor; + badgeValues.message = "N/A"; + } } - } - - // build the svg based on given values - const svg = makeBadge(badgeValues); - - response.type("image/svg+xml"); - response.send(svg); - } catch (error) { - sendHttpError(response, error.message); - } -}); - -router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (request, response) => { - allowAllOrigin(response); - - const { - label, - labelPrefix, - labelSuffix = badgeConstants.defaultUptimeLabelSuffix, - prefix, - suffix = badgeConstants.defaultUptimeValueSuffix, - color, - labelColor, - style = badgeConstants.defaultStyle, - value, // for demo purpose only - } = request.query; - try { - const requestedMonitorId = parseInt(request.params.id, 10); - // if no duration is given, set value to 24 (h) - let requestedDuration = request.params.duration !== undefined ? request.params.duration : "24h"; - const overrideValue = value && parseFloat(value); + // build the svg based on given values + const svg = makeBadge(badgeValues); - if (requestedDuration === "24") { - requestedDuration = "24h"; + response.type("image/svg+xml"); + response.send(svg); + } catch (error) { + sendHttpError(response, error.message); } + } +); + +router.get( + "/api/badge/:id/uptime/:duration?", + cache("5 minutes"), + async (request, response) => { + allowAllOrigin(response); + + const { + label, + labelPrefix, + labelSuffix = badgeConstants.defaultUptimeLabelSuffix, + prefix, + suffix = badgeConstants.defaultUptimeValueSuffix, + color, + labelColor, + style = badgeConstants.defaultStyle, + value, // for demo purpose only + } = request.query; + + try { + const requestedMonitorId = parseInt(request.params.id, 10); + // if no duration is given, set value to 24 (h) + let requestedDuration = + request.params.duration !== undefined + ? request.params.duration + : "24h"; + const overrideValue = value && parseFloat(value); + + if (requestedDuration === "24") { + requestedDuration = "24h"; + } - let publicMonitor = await R.getRow(` + let publicMonitor = await R.getRow( + ` SELECT monitor_group.monitor_id FROM monitor_group, \`group\` WHERE monitor_group.group_id = \`group\`.id AND monitor_group.monitor_id = ? AND public = 1 `, - [ requestedMonitorId ] - ); + [requestedMonitorId] + ); - const badgeValues = { style }; + const badgeValues = { style }; - if (!publicMonitor) { - // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non existent - badgeValues.message = "N/A"; - badgeValues.color = badgeConstants.naColor; - } else { - const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(requestedMonitorId); - const uptime = overrideValue ?? uptimeCalculator.getDataByDuration(requestedDuration).uptime; - - // limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits - const cleanUptime = (uptime * 100).toPrecision(4); - - // use a given, custom color or calculate one based on the uptime value - badgeValues.color = color ?? percentageToColor(uptime); - // use a given, custom labelColor or use the default badge label color (defined by badge-maker) - badgeValues.labelColor = labelColor ?? ""; - // build a label string. If a custom label is given, override the default one (requestedDuration) - badgeValues.label = filterAndJoin([ - labelPrefix, - label ?? `Uptime (${requestedDuration}${labelSuffix})`, - ]); - badgeValues.message = filterAndJoin([ prefix, cleanUptime, suffix ]); - } + if (!publicMonitor) { + // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non existent + badgeValues.message = "N/A"; + badgeValues.color = badgeConstants.naColor; + } else { + const uptimeCalculator = + await UptimeCalculator.getUptimeCalculator( + requestedMonitorId + ); + const uptime = + overrideValue ?? + uptimeCalculator.getDataByDuration(requestedDuration) + .uptime; + + // limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits + const cleanUptime = (uptime * 100).toPrecision(4); + + // use a given, custom color or calculate one based on the uptime value + badgeValues.color = color ?? percentageToColor(uptime); + // use a given, custom labelColor or use the default badge label color (defined by badge-maker) + badgeValues.labelColor = labelColor ?? ""; + // build a label string. If a custom label is given, override the default one (requestedDuration) + badgeValues.label = filterAndJoin([ + labelPrefix, + label ?? `Uptime (${requestedDuration}${labelSuffix})`, + ]); + badgeValues.message = filterAndJoin([ + prefix, + cleanUptime, + suffix, + ]); + } - // build the SVG based on given values - const svg = makeBadge(badgeValues); + // build the SVG based on given values + const svg = makeBadge(badgeValues); - response.type("image/svg+xml"); - response.send(svg); - } catch (error) { - sendHttpError(response, error.message); + response.type("image/svg+xml"); + response.send(svg); + } catch (error) { + sendHttpError(response, error.message); + } } -}); - -router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request, response) => { - allowAllOrigin(response); - - const { - label, - labelPrefix, - labelSuffix = badgeConstants.defaultPingLabelSuffix, - prefix, - suffix = badgeConstants.defaultPingValueSuffix, - color = badgeConstants.defaultPingColor, - labelColor, - style = badgeConstants.defaultStyle, - value, // for demo purpose only - } = request.query; +); + +router.get( + "/api/badge/:id/ping/:duration?", + cache("5 minutes"), + async (request, response) => { + allowAllOrigin(response); + + const { + label, + labelPrefix, + labelSuffix = badgeConstants.defaultPingLabelSuffix, + prefix, + suffix = badgeConstants.defaultPingValueSuffix, + color = badgeConstants.defaultPingColor, + labelColor, + style = badgeConstants.defaultStyle, + value, // for demo purpose only + } = request.query; + + try { + const requestedMonitorId = parseInt(request.params.id, 10); + + // Default duration is 24 (h) if not defined in queryParam, limited to 720h (30d) + let requestedDuration = + request.params.duration !== undefined + ? request.params.duration + : "24h"; + const overrideValue = value && parseFloat(value); + + if (requestedDuration === "24") { + requestedDuration = "24h"; + } - try { - const requestedMonitorId = parseInt(request.params.id, 10); + // Check if monitor is public - // Default duration is 24 (h) if not defined in queryParam, limited to 720h (30d) - let requestedDuration = request.params.duration !== undefined ? request.params.duration : "24h"; - const overrideValue = value && parseFloat(value); + const uptimeCalculator = await UptimeCalculator.getUptimeCalculator( + requestedMonitorId + ); + const publicAvgPing = + uptimeCalculator.getDataByDuration(requestedDuration).avgPing; - if (requestedDuration === "24") { - requestedDuration = "24h"; - } + const badgeValues = { style }; - // Check if monitor is public + if (!publicAvgPing) { + // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant - const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(requestedMonitorId); - const publicAvgPing = uptimeCalculator.getDataByDuration(requestedDuration).avgPing; + badgeValues.message = "N/A"; + badgeValues.color = badgeConstants.naColor; + } else { + const avgPing = parseInt(overrideValue ?? publicAvgPing); - const badgeValues = { style }; + badgeValues.color = color; + // use a given, custom labelColor or use the default badge label color (defined by badge-maker) + badgeValues.labelColor = labelColor ?? ""; + // build a lable string. If a custom label is given, override the default one (requestedDuration) + badgeValues.label = filterAndJoin([ + labelPrefix, + label ?? `Avg. Ping (${requestedDuration}${labelSuffix})`, + ]); + badgeValues.message = filterAndJoin([prefix, avgPing, suffix]); + } - if (!publicAvgPing) { - // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant + // build the SVG based on given values + const svg = makeBadge(badgeValues); - badgeValues.message = "N/A"; - badgeValues.color = badgeConstants.naColor; - } else { - const avgPing = parseInt(overrideValue ?? publicAvgPing); - - badgeValues.color = color; - // use a given, custom labelColor or use the default badge label color (defined by badge-maker) - badgeValues.labelColor = labelColor ?? ""; - // build a lable string. If a custom label is given, override the default one (requestedDuration) - badgeValues.label = filterAndJoin([ labelPrefix, label ?? `Avg. Ping (${requestedDuration}${labelSuffix})` ]); - badgeValues.message = filterAndJoin([ prefix, avgPing, suffix ]); + response.type("image/svg+xml"); + response.send(svg); + } catch (error) { + sendHttpError(response, error.message); } - - // build the SVG based on given values - const svg = makeBadge(badgeValues); - - response.type("image/svg+xml"); - response.send(svg); - } catch (error) { - sendHttpError(response, error.message); } -}); - -router.get("/api/badge/:id/avg-response/:duration?", cache("5 minutes"), async (request, response) => { - allowAllOrigin(response); - - const { - label, - labelPrefix, - labelSuffix, - prefix, - suffix = badgeConstants.defaultPingValueSuffix, - color = badgeConstants.defaultPingColor, - labelColor, - style = badgeConstants.defaultStyle, - value, // for demo purpose only - } = request.query; - - try { - const requestedMonitorId = parseInt(request.params.id, 10); - - // Default duration is 24 (h) if not defined in queryParam, limited to 720h (30d) - const requestedDuration = Math.min( - request.params.duration - ? parseInt(request.params.duration, 10) - : 24, - 720 - ); - const overrideValue = value && parseFloat(value); +); + +router.get( + "/api/badge/:id/avg-response/:duration?", + cache("5 minutes"), + async (request, response) => { + allowAllOrigin(response); + + const { + label, + labelPrefix, + labelSuffix, + prefix, + suffix = badgeConstants.defaultPingValueSuffix, + color = badgeConstants.defaultPingColor, + labelColor, + style = badgeConstants.defaultStyle, + value, // for demo purpose only + } = request.query; + + try { + const requestedMonitorId = parseInt(request.params.id, 10); + + // Default duration is 24 (h) if not defined in queryParam, limited to 720h (30d) + const requestedDuration = Math.min( + request.params.duration + ? parseInt(request.params.duration, 10) + : 24, + 720 + ); + const overrideValue = value && parseFloat(value); - const sqlHourOffset = Database.sqlHourOffset(); + const sqlHourOffset = Database.sqlHourOffset(); - const publicAvgPing = parseInt(await R.getCell(` + const publicAvgPing = parseInt( + await R.getCell( + ` SELECT AVG(ping) FROM monitor_group, \`group\`, heartbeat WHERE monitor_group.group_id = \`group\`.id AND heartbeat.time > ${sqlHourOffset} @@ -377,203 +640,223 @@ router.get("/api/badge/:id/avg-response/:duration?", cache("5 minutes"), async ( AND public = 1 AND heartbeat.monitor_id = ? `, - [ -requestedDuration, requestedMonitorId ] - )); - - const badgeValues = { style }; - - if (!publicAvgPing) { - // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non existent - - badgeValues.message = "N/A"; - badgeValues.color = badgeConstants.naColor; - } else { - const avgPing = parseInt(overrideValue ?? publicAvgPing); - - badgeValues.color = color; - // use a given, custom labelColor or use the default badge label color (defined by badge-maker) - badgeValues.labelColor = labelColor ?? ""; - // build a label string. If a custom label is given, override the default one (requestedDuration) - badgeValues.label = filterAndJoin([ - labelPrefix, - label ?? `Avg. Response (${requestedDuration}h)`, - labelSuffix, - ]); - badgeValues.message = filterAndJoin([ prefix, avgPing, suffix ]); - } - - // build the SVG based on given values - const svg = makeBadge(badgeValues); + [-requestedDuration, requestedMonitorId] + ) + ); - response.type("image/svg+xml"); - response.send(svg); - } catch (error) { - sendHttpError(response, error.message); - } -}); + const badgeValues = { style }; -router.get("/api/badge/:id/cert-exp", cache("5 minutes"), async (request, response) => { - allowAllOrigin(response); + if (!publicAvgPing) { + // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non existent - const date = request.query.date; - - const { - label, - labelPrefix, - labelSuffix, - prefix, - suffix = date ? "" : badgeConstants.defaultCertExpValueSuffix, - upColor = badgeConstants.defaultUpColor, - warnColor = badgeConstants.defaultWarnColor, - downColor = badgeConstants.defaultDownColor, - warnDays = badgeConstants.defaultCertExpireWarnDays, - downDays = badgeConstants.defaultCertExpireDownDays, - labelColor, - style = badgeConstants.defaultStyle, - value, // for demo purpose only - } = request.query; + badgeValues.message = "N/A"; + badgeValues.color = badgeConstants.naColor; + } else { + const avgPing = parseInt(overrideValue ?? publicAvgPing); - try { - const requestedMonitorId = parseInt(request.params.id, 10); + badgeValues.color = color; + // use a given, custom labelColor or use the default badge label color (defined by badge-maker) + badgeValues.labelColor = labelColor ?? ""; + // build a label string. If a custom label is given, override the default one (requestedDuration) + badgeValues.label = filterAndJoin([ + labelPrefix, + label ?? `Avg. Response (${requestedDuration}h)`, + labelSuffix, + ]); + badgeValues.message = filterAndJoin([prefix, avgPing, suffix]); + } - const overrideValue = value && parseFloat(value); + // build the SVG based on given values + const svg = makeBadge(badgeValues); - let publicMonitor = await R.getRow(` + response.type("image/svg+xml"); + response.send(svg); + } catch (error) { + sendHttpError(response, error.message); + } + } +); + +router.get( + "/api/badge/:id/cert-exp", + cache("5 minutes"), + async (request, response) => { + allowAllOrigin(response); + + const date = request.query.date; + + const { + label, + labelPrefix, + labelSuffix, + prefix, + suffix = date ? "" : badgeConstants.defaultCertExpValueSuffix, + upColor = badgeConstants.defaultUpColor, + warnColor = badgeConstants.defaultWarnColor, + downColor = badgeConstants.defaultDownColor, + warnDays = badgeConstants.defaultCertExpireWarnDays, + downDays = badgeConstants.defaultCertExpireDownDays, + labelColor, + style = badgeConstants.defaultStyle, + value, // for demo purpose only + } = request.query; + + try { + const requestedMonitorId = parseInt(request.params.id, 10); + + const overrideValue = value && parseFloat(value); + + let publicMonitor = await R.getRow( + ` SELECT monitor_group.monitor_id FROM monitor_group, \`group\` WHERE monitor_group.group_id = \`group\`.id AND monitor_group.monitor_id = ? AND public = 1 `, - [ requestedMonitorId ] - ); - - const badgeValues = { style }; + [requestedMonitorId] + ); - if (!publicMonitor) { - // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non existent + const badgeValues = { style }; - badgeValues.message = "N/A"; - badgeValues.color = badgeConstants.naColor; - } else { - const tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ - requestedMonitorId, - ]); + if (!publicMonitor) { + // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non existent - if (!tlsInfoBean) { - // return a "No/Bad Cert" badge in naColor (grey), if no cert saved (does not save bad certs?) - badgeValues.message = "No/Bad Cert"; + badgeValues.message = "N/A"; badgeValues.color = badgeConstants.naColor; } else { - const tlsInfo = JSON.parse(tlsInfoBean.info_json); - - if (!tlsInfo.valid) { - // return a "Bad Cert" badge in naColor (grey), when cert is not valid - badgeValues.message = "Bad Cert"; - badgeValues.color = downColor; + const tlsInfoBean = await R.findOne( + "monitor_tls_info", + "monitor_id = ?", + [requestedMonitorId] + ); + + if (!tlsInfoBean) { + // return a "No/Bad Cert" badge in naColor (grey), if no cert saved (does not save bad certs?) + badgeValues.message = "No/Bad Cert"; + badgeValues.color = badgeConstants.naColor; } else { - const daysRemaining = parseInt(overrideValue ?? tlsInfo.certInfo.daysRemaining); + const tlsInfo = JSON.parse(tlsInfoBean.info_json); - if (daysRemaining > warnDays) { - badgeValues.color = upColor; - } else if (daysRemaining > downDays) { - badgeValues.color = warnColor; - } else { + if (!tlsInfo.valid) { + // return a "Bad Cert" badge in naColor (grey), when cert is not valid + badgeValues.message = "Bad Cert"; badgeValues.color = downColor; + } else { + const daysRemaining = parseInt( + overrideValue ?? tlsInfo.certInfo.daysRemaining + ); + + if (daysRemaining > warnDays) { + badgeValues.color = upColor; + } else if (daysRemaining > downDays) { + badgeValues.color = warnColor; + } else { + badgeValues.color = downColor; + } + // use a given, custom labelColor or use the default badge label color (defined by badge-maker) + badgeValues.labelColor = labelColor ?? ""; + // build a label string. If a custom label is given, override the default one + badgeValues.label = filterAndJoin([ + labelPrefix, + label ?? "Cert Exp.", + labelSuffix, + ]); + badgeValues.message = filterAndJoin([ + prefix, + date ? tlsInfo.certInfo.validTo : daysRemaining, + suffix, + ]); } - // use a given, custom labelColor or use the default badge label color (defined by badge-maker) - badgeValues.labelColor = labelColor ?? ""; - // build a label string. If a custom label is given, override the default one - badgeValues.label = filterAndJoin([ - labelPrefix, - label ?? "Cert Exp.", - labelSuffix, - ]); - badgeValues.message = filterAndJoin([ prefix, date ? tlsInfo.certInfo.validTo : daysRemaining, suffix ]); } } - } - // build the SVG based on given values - const svg = makeBadge(badgeValues); + // build the SVG based on given values + const svg = makeBadge(badgeValues); - response.type("image/svg+xml"); - response.send(svg); - } catch (error) { - sendHttpError(response, error.message); + response.type("image/svg+xml"); + response.send(svg); + } catch (error) { + sendHttpError(response, error.message); + } } -}); - -router.get("/api/badge/:id/response", cache("5 minutes"), async (request, response) => { - allowAllOrigin(response); - - const { - label, - labelPrefix, - labelSuffix, - prefix, - suffix = badgeConstants.defaultPingValueSuffix, - color = badgeConstants.defaultPingColor, - labelColor, - style = badgeConstants.defaultStyle, - value, // for demo purpose only - } = request.query; - - try { - const requestedMonitorId = parseInt(request.params.id, 10); - - const overrideValue = value && parseFloat(value); - - let publicMonitor = await R.getRow(` +); + +router.get( + "/api/badge/:id/response", + cache("5 minutes"), + async (request, response) => { + allowAllOrigin(response); + + const { + label, + labelPrefix, + labelSuffix, + prefix, + suffix = badgeConstants.defaultPingValueSuffix, + color = badgeConstants.defaultPingColor, + labelColor, + style = badgeConstants.defaultStyle, + value, // for demo purpose only + } = request.query; + + try { + const requestedMonitorId = parseInt(request.params.id, 10); + + const overrideValue = value && parseFloat(value); + + let publicMonitor = await R.getRow( + ` SELECT monitor_group.monitor_id FROM monitor_group, \`group\` WHERE monitor_group.group_id = \`group\`.id AND monitor_group.monitor_id = ? AND public = 1 `, - [ requestedMonitorId ] - ); - - const badgeValues = { style }; - - if (!publicMonitor) { - // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non existent - - badgeValues.message = "N/A"; - badgeValues.color = badgeConstants.naColor; - } else { - const heartbeat = await Monitor.getPreviousHeartbeat( - requestedMonitorId + [requestedMonitorId] ); - if (!heartbeat.ping) { - // return a "N/A" badge in naColor (grey), if previous heartbeat has no ping + const badgeValues = { style }; + + if (!publicMonitor) { + // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non existent badgeValues.message = "N/A"; badgeValues.color = badgeConstants.naColor; } else { - const ping = parseInt(overrideValue ?? heartbeat.ping); + const heartbeat = await Monitor.getPreviousHeartbeat( + requestedMonitorId + ); - badgeValues.color = color; - // use a given, custom labelColor or use the default badge label color (defined by badge-maker) - badgeValues.labelColor = labelColor ?? ""; - // build a label string. If a custom label is given, override the default one - badgeValues.label = filterAndJoin([ - labelPrefix, - label ?? "Response", - labelSuffix, - ]); - badgeValues.message = filterAndJoin([ prefix, ping, suffix ]); + if (!heartbeat.ping) { + // return a "N/A" badge in naColor (grey), if previous heartbeat has no ping + + badgeValues.message = "N/A"; + badgeValues.color = badgeConstants.naColor; + } else { + const ping = parseInt(overrideValue ?? heartbeat.ping); + + badgeValues.color = color; + // use a given, custom labelColor or use the default badge label color (defined by badge-maker) + badgeValues.labelColor = labelColor ?? ""; + // build a label string. If a custom label is given, override the default one + badgeValues.label = filterAndJoin([ + labelPrefix, + label ?? "Response", + labelSuffix, + ]); + badgeValues.message = filterAndJoin([prefix, ping, suffix]); + } } - } - // build the SVG based on given values - const svg = makeBadge(badgeValues); + // build the SVG based on given values + const svg = makeBadge(badgeValues); - response.type("image/svg+xml"); - response.send(svg); - } catch (error) { - sendHttpError(response, error.message); + response.type("image/svg+xml"); + response.send(svg); + } catch (error) { + sendHttpError(response, error.message); + } } -}); +); /** * Determines the status of the next beat in the push route handling. @@ -584,7 +867,13 @@ router.get("/api/badge/:id/response", cache("5 minutes"), async (request, respon * @param {object} bean - The new heartbeat object. * @returns {void} */ -function determineStatus(status, previousHeartbeat, maxretries, isUpsideDown, bean) { +function determineStatus( + status, + previousHeartbeat, + maxretries, + isUpsideDown, + bean +) { if (isUpsideDown) { status = flipStatus(status); } @@ -592,7 +881,7 @@ function determineStatus(status, previousHeartbeat, maxretries, isUpsideDown, be if (previousHeartbeat) { if (previousHeartbeat.status === UP && status === DOWN) { // Going Down - if ((maxretries > 0) && (previousHeartbeat.retries < maxretries)) { + if (maxretries > 0 && previousHeartbeat.retries < maxretries) { // Retries available bean.retries = previousHeartbeat.retries + 1; bean.status = PENDING; @@ -601,7 +890,11 @@ function determineStatus(status, previousHeartbeat, maxretries, isUpsideDown, be bean.retries = 0; bean.status = DOWN; } - } else if (previousHeartbeat.status === PENDING && status === DOWN && previousHeartbeat.retries < maxretries) { + } else if ( + previousHeartbeat.status === PENDING && + status === DOWN && + previousHeartbeat.retries < maxretries + ) { // Retries available bean.retries = previousHeartbeat.retries + 1; bean.status = PENDING; diff --git a/server/server.js b/server/server.js index eeffb06356..28a71dcb98 100644 --- a/server/server.js +++ b/server/server.js @@ -697,6 +697,7 @@ let needSetup = false; await server.sendMonitorList(socket); if (monitor.active !== false) { + log.debug("monitor", "Start Monitor: " + bean.id); await startMonitor(socket.userID, bean.id); } @@ -755,6 +756,8 @@ let needSetup = false; bean.parent = monitor.parent; bean.type = monitor.type; bean.url = monitor.url; + bean.restartUrl = monitor.restartUrl; + bean.restartInterval = monitor.restartInterval; bean.method = monitor.method; bean.body = monitor.body; bean.headers = monitor.headers; @@ -1752,7 +1755,7 @@ async function startMonitors() { for (let monitor of list) { server.monitorList[monitor.id] = monitor; } - + log.debug("server", "Start all monitors"); for (let monitor of list) { monitor.start(io); // Give some delays, so all monitors won't make request at the same moment when just start the server. diff --git a/src/components/MonitorListItem.vue b/src/components/MonitorListItem.vue index 9b45ae9f28..0a24807f90 100644 --- a/src/components/MonitorListItem.vue +++ b/src/components/MonitorListItem.vue @@ -12,30 +12,83 @@ /> - +
-
+
- - + + {{ monitor.name }}
- +
-
- +
+
- +
+
@@ -43,7 +96,8 @@
{} + default: () => {}, }, /** Callback fired when monitor is selected */ select: { type: Function, - default: () => {} + default: () => {}, }, /** Callback fired when monitor is deselected */ deselect: { type: Function, - default: () => {} + default: () => {}, }, /** Function to filter child monitors */ filterFunc: { type: Function, - default: () => {} + default: () => {}, }, /** Function to sort child monitors */ sortFunc: { type: Function, default: () => {}, - } + }, }, data() { return { isCollapsed: true, + isRestarting: false, }; }, computed: { @@ -120,7 +176,9 @@ export default { let result = Object.values(this.$root.monitorList); // Get children - result = result.filter(childMonitor => childMonitor.parent === this.monitor.id); + result = result.filter( + (childMonitor) => childMonitor.parent === this.monitor.id + ); // Run filter on children result = result.filter(this.filterFunc); @@ -142,12 +200,13 @@ export default { isSelectMode() { // TODO: Resize the heartbeat bar, but too slow // this.$refs.heartbeatBar.resize(); - } + }, }, beforeMount() { - // Always unfold if monitor is accessed directly - if (this.monitor.childrenIDs.includes(parseInt(this.$route.params.id))) { + if ( + this.monitor.childrenIDs.includes(parseInt(this.$route.params.id)) + ) { this.isCollapsed = false; return; } @@ -166,6 +225,20 @@ export default { this.isCollapsed = storageObject[`monitor_${this.monitor.id}`]; }, methods: { + restartMonitor(id) { + this.isRestarting = true; + axios + .post(`/api/monitor/${id}/restart`) + .then((response) => { + this.$root.toastSuccess("Monitor restarted."); + }) + .catch((error) => { + this.$root.toastError(error.msg); + }) + .finally(() => { + this.isRestarting = false; + }); + }, /** * Changes the collapsed value of the current monitor and saves * it to local storage @@ -182,7 +255,10 @@ export default { } storageObject[`monitor_${this.monitor.id}`] = this.isCollapsed; - window.localStorage.setItem("monitorCollapsed", JSON.stringify(storageObject)); + window.localStorage.setItem( + "monitorCollapsed", + JSON.stringify(storageObject) + ); }, /** * Get URL of monitor @@ -249,5 +325,4 @@ export default { position: relative; z-index: 15; } - diff --git a/src/components/PublicGroupList.vue b/src/components/PublicGroupList.vue index d1c1f4c527..075fceac7c 100644 --- a/src/components/PublicGroupList.vue +++ b/src/components/PublicGroupList.vue @@ -7,16 +7,32 @@ :animation="100" >
@@ -263,25 +497,43 @@ {{ $t("Description") }}: - + -
+
- @@ -302,46 +554,90 @@ >
- {{ $t("No monitors available.") }} {{ $t("Add one") }} + {{ $t("No monitors available.") }} + {{ $t("Add one") }}
-
+
👀 {{ $t("statusPageNothing") }}
- +
- + -
+

- {{ $t("Powered by") }} {{ $t("Uptime Kuma" ) }} + {{ $t("Powered by") }} + {{ $t("Uptime Kuma") }}

-
{{ $t("Last Updated") }}: {{ lastUpdateTimeDisplay }}
-
{{ $tc("statusPageRefreshIn", [ updateCountdownText]) }}
+
+ {{ $t("Last Updated") }}: {{ lastUpdateTimeDisplay }} +
+
+ {{ $tc("statusPageRefreshIn", [updateCountdownText]) }} +
- + {{ $t("deleteStatusPageMsg") }} @@ -371,7 +667,14 @@ import Confirm from "../components/Confirm.vue"; import PublicGroupList from "../components/PublicGroupList.vue"; import MaintenanceTime from "../components/MaintenanceTime.vue"; import { getResBaseURL } from "../util-frontend"; -import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE } from "../util.ts"; +import { + STATUS_PAGE_ALL_DOWN, + STATUS_PAGE_ALL_UP, + STATUS_PAGE_MAINTENANCE, + STATUS_PAGE_PARTIAL_DOWN, + UP, + MAINTENANCE, +} from "../util.ts"; import Tag from "../components/Tag.vue"; import VueMultiselect from "vue-multiselect"; @@ -384,11 +687,10 @@ const leavePageMsg = "Do you really want to leave? you have unsaved changes!"; let feedInterval; const favicon = new Favico({ - animation: "none" + animation: "none", }); export default { - components: { PublicGroupList, ImageCropUpload, @@ -396,7 +698,7 @@ export default { PrismEditor, MaintenanceTime, Tag, - VueMultiselect + VueMultiselect, }, // Leave Page for vue route change @@ -446,7 +748,6 @@ export default { }; }, computed: { - logoURL() { if (this.imgDataUrl.startsWith("data:")) { return this.imgDataUrl; @@ -463,14 +764,16 @@ export default { let result = []; for (let id in this.$root.monitorList) { - if (this.$root.monitorList[id] && ! (id in this.$root.publicMonitorList)) { + if ( + this.$root.monitorList[id] && + !(id in this.$root.publicMonitorList) + ) { let monitor = this.$root.monitorList[id]; result.push(monitor); } } result.sort((m1, m2) => { - if (m1.active !== m2.active) { if (m1.active === 0) { return 1; @@ -527,7 +830,6 @@ export default { }, overallStatus() { - if (Object.keys(this.$root.publicLastHeartbeatList).length === 0) { return -1; } @@ -547,7 +849,7 @@ export default { } } - if (! hasUp) { + if (!hasUp) { status = STATUS_PAGE_ALL_DOWN; } @@ -596,10 +898,9 @@ export default { lastUpdateTimeDisplay() { return this.$root.datetime(this.lastUpdateTime); - } + }, }, watch: { - /** * If connected to the socket and logged in, request private data of this statusPage * @param {boolean} loggedIn Is the client logged in? @@ -607,20 +908,20 @@ export default { */ "$root.loggedIn"(loggedIn) { if (loggedIn) { - this.$root.getSocket().emit("getStatusPage", this.slug, (res) => { - if (res.ok) { - this.config = res.config; - - if (!this.config.customCSS) { - this.config.customCSS = "body {\n" + - " \n" + - "}\n"; + this.$root + .getSocket() + .emit("getStatusPage", this.slug, (res) => { + if (res.ok) { + this.config = res.config; + + if (!this.config.customCSS) { + this.config.customCSS = + "body {\n" + " \n" + "}\n"; + } + } else { + this.$root.toastError(res.msg); } - - } else { - this.$root.toastError(res.msg); - } - }); + }); } }, @@ -659,17 +960,20 @@ export default { if (count > 0) { for (let group of this.$root.publicGroupList) { for (let monitor of group.monitorList) { - if (monitor.tags === undefined && this.$root.monitorList[monitor.id]) { - monitor.tags = this.$root.monitorList[monitor.id].tags; + if ( + monitor.tags === undefined && + this.$root.monitorList[monitor.id] + ) { + monitor.tags = + this.$root.monitorList[monitor.id].tags; } } } } - } - + }, }, async created() { - this.hasToken = ("token" in this.$root.storage()); + this.hasToken = "token" in this.$root.storage(); // Browser change page // https://stackoverflow.com/questions/7317273/warn-user-before-leaving-web-page-with-unsaved-changes @@ -692,28 +996,30 @@ export default { this.slug = "default"; } - this.getData().then((res) => { - this.config = res.data.config; + this.getData() + .then((res) => { + this.config = res.data.config; - if (!this.config.domainNameList) { - this.config.domainNameList = []; - } + if (!this.config.domainNameList) { + this.config.domainNameList = []; + } - if (this.config.icon) { - this.imgDataUrl = this.config.icon; - } + if (this.config.icon) { + this.imgDataUrl = this.config.icon; + } - this.incident = res.data.incident; - this.maintenanceList = res.data.maintenanceList; - this.$root.publicGroupList = res.data.publicGroupList; + this.incident = res.data.incident; + this.maintenanceList = res.data.maintenanceList; + this.$root.publicGroupList = res.data.publicGroupList; - this.loading = false; - }).catch( function (error) { - if (error.response.status === 404) { - location.href = "/page-not-found"; - } - console.log(error); - }); + this.loading = false; + }) + .catch(function (error) { + if (error.response.status === 404) { + location.href = "/page-not-found"; + } + console.log(error); + }); // Configure auto-refresh loop this.updateHeartbeatList(); @@ -730,7 +1036,6 @@ export default { } }, methods: { - /** * Get status page data * It should be preloaded in window.preloadData @@ -738,9 +1043,11 @@ export default { */ getData: function () { if (window.preloadData) { - return new Promise(resolve => resolve({ - data: window.preloadData - })); + return new Promise((resolve) => + resolve({ + data: window.preloadData, + }) + ); } else { return axios.get("/api/status-page/" + this.slug); } @@ -761,31 +1068,39 @@ export default { */ updateHeartbeatList() { // If editMode, it will use the data from websocket. - if (! this.editMode) { - axios.get("/api/status-page/heartbeat/" + this.slug).then((res) => { - const { heartbeatList, uptimeList } = res.data; - - this.$root.heartbeatList = heartbeatList; - this.$root.uptimeList = uptimeList; - - const heartbeatIds = Object.keys(heartbeatList); - const downMonitors = heartbeatIds.reduce((downMonitorsAmount, currentId) => { - const monitorHeartbeats = heartbeatList[currentId]; - const lastHeartbeat = monitorHeartbeats.at(-1); - - if (lastHeartbeat) { - return lastHeartbeat.status === 0 ? downMonitorsAmount + 1 : downMonitorsAmount; - } else { - return downMonitorsAmount; - } - }, 0); - - favicon.badge(downMonitors); - - this.loadedData = true; - this.lastUpdateTime = dayjs(); - this.updateUpdateTimer(); - }); + if (!this.editMode) { + axios + .get("/api/status-page/heartbeat/" + this.slug) + .then((res) => { + const { heartbeatList, uptimeList } = res.data; + + this.$root.heartbeatList = heartbeatList; + this.$root.uptimeList = uptimeList; + + const heartbeatIds = Object.keys(heartbeatList); + const downMonitors = heartbeatIds.reduce( + (downMonitorsAmount, currentId) => { + const monitorHeartbeats = + heartbeatList[currentId]; + const lastHeartbeat = monitorHeartbeats.at(-1); + + if (lastHeartbeat) { + return lastHeartbeat.status === 0 + ? downMonitorsAmount + 1 + : downMonitorsAmount; + } else { + return downMonitorsAmount; + } + }, + 0 + ); + + favicon.badge(downMonitors); + + this.loadedData = true; + this.lastUpdateTime = dayjs(); + this.updateUpdateTimer(); + }); } }, @@ -797,7 +1112,12 @@ export default { clearInterval(this.updateCountdown); this.updateCountdown = setInterval(() => { - const countdown = dayjs.duration(this.lastUpdateTime.add(this.autoRefreshInterval, "minutes").add(10, "seconds").diff(dayjs())); + const countdown = dayjs.duration( + this.lastUpdateTime + .add(this.autoRefreshInterval, "minutes") + .add(10, "seconds") + .diff(dayjs()) + ); if (countdown.as("seconds") < 0) { clearInterval(this.updateCountdown); } else { @@ -830,29 +1150,37 @@ export default { let startTime = new Date(); this.config.slug = this.config.slug.trim().toLowerCase(); - this.$root.getSocket().emit("saveStatusPage", this.slug, this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => { - if (res.ok) { - this.enableEditMode = false; - this.$root.publicGroupList = res.publicGroupList; - - // Add some delay, so that the side menu animation would be better - let endTime = new Date(); - let time = 100 - (endTime - startTime) / 1000; - - if (time < 0) { - time = 0; + this.$root + .getSocket() + .emit( + "saveStatusPage", + this.slug, + this.config, + this.imgDataUrl, + this.$root.publicGroupList, + (res) => { + if (res.ok) { + this.enableEditMode = false; + this.$root.publicGroupList = res.publicGroupList; + + // Add some delay, so that the side menu animation would be better + let endTime = new Date(); + let time = 100 - (endTime - startTime) / 1000; + + if (time < 0) { + time = 0; + } + + setTimeout(() => { + this.loading = false; + location.href = "/status/" + this.config.slug; + }, time); + } else { + this.loading = false; + toast.error(res.msg); + } } - - setTimeout(() => { - this.loading = false; - location.href = "/status/" + this.config.slug; - }, time); - - } else { - this.loading = false; - toast.error(res.msg); - } - }); + ); }, /** @@ -868,14 +1196,16 @@ export default { * @returns {void} */ deleteStatusPage() { - this.$root.getSocket().emit("deleteStatusPage", this.slug, (res) => { - if (res.ok) { - this.enableEditMode = false; - location.href = "/manage-status-page"; - } else { - this.$root.toastError(res.msg); - } - }); + this.$root + .getSocket() + .emit("deleteStatusPage", this.slug, (res) => { + if (res.ok) { + this.enableEditMode = false; + location.href = "/manage-status-page"; + } else { + this.$root.toastError(res.msg); + } + }); }, /** @@ -967,17 +1297,16 @@ export default { return; } - this.$root.getSocket().emit("postIncident", this.slug, this.incident, (res) => { - - if (res.ok) { - this.enableEditIncidentMode = false; - this.incident = res.incident; - } else { - this.$root.toastError(res.msg); - } - - }); - + this.$root + .getSocket() + .emit("postIncident", this.slug, this.incident, (res) => { + if (res.ok) { + this.enableEditIncidentMode = false; + this.incident = res.incident; + } else { + this.$root.toastError(res.msg); + } + }); }, /** @@ -1042,8 +1371,7 @@ export default { return ""; } }, - - } + }, }; @@ -1258,5 +1586,4 @@ footer { .refresh-info { opacity: 0.7; } - diff --git a/src/util-frontend.js b/src/util-frontend.js index 1e30160ea4..3e08724441 100644 --- a/src/util-frontend.js +++ b/src/util-frontend.js @@ -65,8 +65,8 @@ export function timezoneList() { */ export function setPageLocale() { const html = document.documentElement; - html.setAttribute("lang", currentLocale() ); - html.setAttribute("dir", localeDirection() ); + html.setAttribute("lang", currentLocale()); + html.setAttribute("dir", localeDirection()); } /** @@ -91,7 +91,7 @@ export function getResBaseURL() { */ export function isDevContainer() { // eslint-disable-next-line no-undef - return (typeof DEVCONTAINER === "string" && DEVCONTAINER === "1"); + return typeof DEVCONTAINER === "string" && DEVCONTAINER === "1"; } /** @@ -117,9 +117,13 @@ export function hostNameRegexPattern(mqtt = false) { // mqtt, mqtts, ws and wss schemes accepted by mqtt.js (https://github.com/mqttjs/MQTT.js/#connect) const mqttSchemeRegexPattern = "((mqtt|ws)s?:\\/\\/)?"; // Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/ - const ipRegexPattern = `((^${mqtt ? mqttSchemeRegexPattern : ""}((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?$))`; + const ipRegexPattern = `((^${ + mqtt ? mqttSchemeRegexPattern : "" + }((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?$))`; // Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address - const hostNameRegexPattern = `^${mqtt ? mqttSchemeRegexPattern : ""}([a-zA-Z0-9])?(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])(\\.)?$`; + const hostNameRegexPattern = `^${ + mqtt ? mqttSchemeRegexPattern : "" + }([a-zA-Z0-9])?(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])(\\.)?$`; return `${ipRegexPattern}|${hostNameRegexPattern}`; } @@ -132,22 +136,38 @@ export function hostNameRegexPattern(mqtt = false) { */ export function colorOptions(self) { return [ - { name: self.$t("Gray"), - color: "#4B5563" }, - { name: self.$t("Red"), - color: "#DC2626" }, - { name: self.$t("Orange"), - color: "#D97706" }, - { name: self.$t("Green"), - color: "#059669" }, - { name: self.$t("Blue"), - color: "#2563EB" }, - { name: self.$t("Indigo"), - color: "#4F46E5" }, - { name: self.$t("Purple"), - color: "#7C3AED" }, - { name: self.$t("Pink"), - color: "#DB2777" }, + { + name: self.$t("Gray"), + color: "#4B5563", + }, + { + name: self.$t("Red"), + color: "#DC2626", + }, + { + name: self.$t("Orange"), + color: "#D97706", + }, + { + name: self.$t("Green"), + color: "#059669", + }, + { + name: self.$t("Blue"), + color: "#2563EB", + }, + { + name: self.$t("Indigo"), + color: "#4F46E5", + }, + { + name: self.$t("Purple"), + color: "#7C3AED", + }, + { + name: self.$t("Pink"), + color: "#DB2777", + }, ]; }