75 changed files with 4024 additions and 890 deletions
			
			
		
								
									Binary file not shown.
								
							
						
					| @ -0,0 +1,30 @@ | |||||
|  | -- You should not modify if this have pushed to Github, unless it does serious wrong with the db. | ||||
|  | BEGIN TRANSACTION; | ||||
|  | 
 | ||||
|  | create table `group` | ||||
|  | ( | ||||
|  |     id           INTEGER      not null | ||||
|  |         constraint group_pk | ||||
|  |             primary key autoincrement, | ||||
|  |     name         VARCHAR(255) not null, | ||||
|  |     created_date DATETIME              default (DATETIME('now')) not null, | ||||
|  |     public       BOOLEAN               default 0 not null, | ||||
|  |     active       BOOLEAN               default 1 not null, | ||||
|  |     weight       BOOLEAN      NOT NULL DEFAULT 1000 | ||||
|  | ); | ||||
|  | 
 | ||||
|  | CREATE TABLE [monitor_group] | ||||
|  | ( | ||||
|  |     [id]         INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, | ||||
|  |     [monitor_id] INTEGER                           NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE, | ||||
|  |     [group_id]   INTEGER                           NOT NULL REFERENCES [group] ([id]) ON DELETE CASCADE ON UPDATE CASCADE, | ||||
|  |     weight BOOLEAN NOT NULL DEFAULT 1000 | ||||
|  | ); | ||||
|  | 
 | ||||
|  | CREATE INDEX [fk] | ||||
|  |     ON [monitor_group] ( | ||||
|  |                         [monitor_id], | ||||
|  |                         [group_id]); | ||||
|  | 
 | ||||
|  | 
 | ||||
|  | COMMIT; | ||||
| @ -0,0 +1,18 @@ | |||||
|  | -- You should not modify if this have pushed to Github, unless it does serious wrong with the db. | ||||
|  | BEGIN TRANSACTION; | ||||
|  | 
 | ||||
|  | create table incident | ||||
|  | ( | ||||
|  |     id INTEGER not null | ||||
|  |         constraint incident_pk | ||||
|  |             primary key autoincrement, | ||||
|  |     title VARCHAR(255) not null, | ||||
|  |     content TEXT not null, | ||||
|  |     style VARCHAR(30) default 'warning' not null, | ||||
|  |     created_date DATETIME default (DATETIME('now')) not null, | ||||
|  |     last_updated_date DATETIME, | ||||
|  |     pin BOOLEAN default 1 not null, | ||||
|  |     active BOOLEAN default 1 not null | ||||
|  | ); | ||||
|  | 
 | ||||
|  | COMMIT; | ||||
| @ -0,0 +1,21 @@ | |||||
|  | #!/usr/bin/env sh | ||||
|  | 
 | ||||
|  | # set -e Exit the script if an error happens | ||||
|  | set -e | ||||
|  | PUID=${PUID=1000} | ||||
|  | PGID=${PGID=1000} | ||||
|  | 
 | ||||
|  | files_ownership () { | ||||
|  |     # -h Changes the ownership of an encountered symbolic link and not that of the file or directory pointed to by the symbolic link. | ||||
|  |     # -R Recursively descends the specified directories | ||||
|  |     # -c Like verbose but report only when a change is made | ||||
|  |     chown -hRc "$PUID":"$PGID" /app/data | ||||
|  | } | ||||
|  | 
 | ||||
|  | echo "==> Performing startup jobs and maintenance tasks" | ||||
|  | files_ownership | ||||
|  | 
 | ||||
|  | echo "==> Starting application with user $PUID group $PGID" | ||||
|  | 
 | ||||
|  | # --clear-groups Clear supplementary groups. | ||||
|  | exec setpriv --reuid "$PUID" --regid "$PGID" --clear-groups "$@" | ||||
| After Width: | Height: | Size: 2.6 KiB | 
| After Width: | Height: | Size: 9.5 KiB | 
| @ -0,0 +1,19 @@ | |||||
|  | { | ||||
|  |     "name": "Uptime Kuma", | ||||
|  |     "short_name": "Uptime Kuma", | ||||
|  |     "start_url": "/", | ||||
|  |     "background_color": "#fff", | ||||
|  |     "display": "standalone", | ||||
|  |     "icons": [ | ||||
|  |         { | ||||
|  |             "src": "icon-192x192.png", | ||||
|  |             "sizes": "192x192", | ||||
|  |             "type": "image/png" | ||||
|  |         }, | ||||
|  |         { | ||||
|  |             "src": "icon-512x512.png", | ||||
|  |             "sizes": "512x512", | ||||
|  |             "type": "image/png" | ||||
|  |         } | ||||
|  |     ] | ||||
|  | } | ||||
| @ -0,0 +1,57 @@ | |||||
|  | /* | ||||
|  |     From https://github.com/DiegoZoracKy/image-data-uri/blob/master/lib/image-data-uri.js
 | ||||
|  |     Modified with 0 dependencies | ||||
|  |  */ | ||||
|  | let fs = require("fs"); | ||||
|  | 
 | ||||
|  | let ImageDataURI = (() => { | ||||
|  | 
 | ||||
|  |     function decode(dataURI) { | ||||
|  |         if (!/data:image\//.test(dataURI)) { | ||||
|  |             console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\""); | ||||
|  |             return null; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         let regExMatches = dataURI.match("data:(image/.*);base64,(.*)"); | ||||
|  |         return { | ||||
|  |             imageType: regExMatches[1], | ||||
|  |             dataBase64: regExMatches[2], | ||||
|  |             dataBuffer: new Buffer(regExMatches[2], "base64") | ||||
|  |         }; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     function encode(data, mediaType) { | ||||
|  |         if (!data || !mediaType) { | ||||
|  |             console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType "); | ||||
|  |             return null; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         mediaType = (/\//.test(mediaType)) ? mediaType : "image/" + mediaType; | ||||
|  |         let dataBase64 = (Buffer.isBuffer(data)) ? data.toString("base64") : new Buffer(data).toString("base64"); | ||||
|  |         let dataImgBase64 = "data:" + mediaType + ";base64," + dataBase64; | ||||
|  | 
 | ||||
|  |         return dataImgBase64; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     function outputFile(dataURI, filePath) { | ||||
|  |         filePath = filePath || "./"; | ||||
|  |         return new Promise((resolve, reject) => { | ||||
|  |             let imageDecoded = decode(dataURI); | ||||
|  | 
 | ||||
|  |             fs.writeFile(filePath, imageDecoded.dataBuffer, err => { | ||||
|  |                 if (err) { | ||||
|  |                     return reject("ImageDataURI :: Error :: " + JSON.stringify(err, null, 4)); | ||||
|  |                 } | ||||
|  |                 resolve(filePath); | ||||
|  |             }); | ||||
|  |         }); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     return { | ||||
|  |         decode: decode, | ||||
|  |         encode: encode, | ||||
|  |         outputFile: outputFile, | ||||
|  |     }; | ||||
|  | })(); | ||||
|  | 
 | ||||
|  | module.exports = ImageDataURI; | ||||
| @ -0,0 +1,34 @@ | |||||
|  | const { BeanModel } = require("redbean-node/dist/bean-model"); | ||||
|  | const { R } = require("redbean-node"); | ||||
|  | 
 | ||||
|  | class Group extends BeanModel { | ||||
|  | 
 | ||||
|  |     async toPublicJSON() { | ||||
|  |         let monitorBeanList = await this.getMonitorList(); | ||||
|  |         let monitorList = []; | ||||
|  | 
 | ||||
|  |         for (let bean of monitorBeanList) { | ||||
|  |             monitorList.push(await bean.toPublicJSON()); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         return { | ||||
|  |             id: this.id, | ||||
|  |             name: this.name, | ||||
|  |             weight: this.weight, | ||||
|  |             monitorList, | ||||
|  |         }; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     async getMonitorList() { | ||||
|  |         return R.convertToBeans("monitor", await R.getAll(` | ||||
|  |             SELECT monitor.* FROM monitor, monitor_group | ||||
|  |             WHERE monitor.id = monitor_group.monitor_id | ||||
|  |             AND group_id = ? | ||||
|  |             ORDER BY monitor_group.weight | ||||
|  |         `, [
 | ||||
|  |             this.id, | ||||
|  |         ])); | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | module.exports = Group; | ||||
| @ -0,0 +1,18 @@ | |||||
|  | const { BeanModel } = require("redbean-node/dist/bean-model"); | ||||
|  | 
 | ||||
|  | class Incident extends BeanModel { | ||||
|  | 
 | ||||
|  |     toPublicJSON() { | ||||
|  |         return { | ||||
|  |             id: this.id, | ||||
|  |             style: this.style, | ||||
|  |             title: this.title, | ||||
|  |             content: this.content, | ||||
|  |             pin: this.pin, | ||||
|  |             createdDate: this.createdDate, | ||||
|  |             lastUpdatedDate: this.lastUpdatedDate, | ||||
|  |         }; | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | module.exports = Incident; | ||||
| @ -0,0 +1,749 @@ | |||||
|  | let url = require("url"); | ||||
|  | let MemoryCache = require("./memory-cache"); | ||||
|  | 
 | ||||
|  | let t = { | ||||
|  |     ms: 1, | ||||
|  |     second: 1000, | ||||
|  |     minute: 60000, | ||||
|  |     hour: 3600000, | ||||
|  |     day: 3600000 * 24, | ||||
|  |     week: 3600000 * 24 * 7, | ||||
|  |     month: 3600000 * 24 * 30, | ||||
|  | }; | ||||
|  | 
 | ||||
|  | let instances = []; | ||||
|  | 
 | ||||
|  | let matches = function (a) { | ||||
|  |     return function (b) { | ||||
|  |         return a === b; | ||||
|  |     }; | ||||
|  | }; | ||||
|  | 
 | ||||
|  | let doesntMatch = function (a) { | ||||
|  |     return function (b) { | ||||
|  |         return !matches(a)(b); | ||||
|  |     }; | ||||
|  | }; | ||||
|  | 
 | ||||
|  | let logDuration = function (d, prefix) { | ||||
|  |     let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms"; | ||||
|  |     return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m"; | ||||
|  | }; | ||||
|  | 
 | ||||
|  | function getSafeHeaders(res) { | ||||
|  |     return res.getHeaders ? res.getHeaders() : res._headers; | ||||
|  | } | ||||
|  | 
 | ||||
|  | function ApiCache() { | ||||
|  |     let memCache = new MemoryCache(); | ||||
|  | 
 | ||||
|  |     let globalOptions = { | ||||
|  |         debug: false, | ||||
|  |         defaultDuration: 3600000, | ||||
|  |         enabled: true, | ||||
|  |         appendKey: [], | ||||
|  |         jsonp: false, | ||||
|  |         redisClient: false, | ||||
|  |         headerBlacklist: [], | ||||
|  |         statusCodes: { | ||||
|  |             include: [], | ||||
|  |             exclude: [], | ||||
|  |         }, | ||||
|  |         events: { | ||||
|  |             expire: undefined, | ||||
|  |         }, | ||||
|  |         headers: { | ||||
|  |             // 'cache-control':  'no-cache' // example of header overwrite
 | ||||
|  |         }, | ||||
|  |         trackPerformance: false, | ||||
|  |         respectCacheControl: false, | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     let middlewareOptions = []; | ||||
|  |     let instance = this; | ||||
|  |     let index = null; | ||||
|  |     let timers = {}; | ||||
|  |     let performanceArray = []; // for tracking cache hit rate
 | ||||
|  | 
 | ||||
|  |     instances.push(this); | ||||
|  |     this.id = instances.length; | ||||
|  | 
 | ||||
|  |     function debug(a, b, c, d) { | ||||
|  |         let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) { | ||||
|  |             return arg !== undefined; | ||||
|  |         }); | ||||
|  |         let debugEnv = process.env.DEBUG && process.env.DEBUG.split(",").indexOf("apicache") !== -1; | ||||
|  | 
 | ||||
|  |         return (globalOptions.debug || debugEnv) && console.log.apply(null, arr); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     function shouldCacheResponse(request, response, toggle) { | ||||
|  |         let opt = globalOptions; | ||||
|  |         let codes = opt.statusCodes; | ||||
|  | 
 | ||||
|  |         if (!response) { | ||||
|  |             return false; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         if (toggle && !toggle(request, response)) { | ||||
|  |             return false; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         if (codes.exclude && codes.exclude.length && codes.exclude.indexOf(response.statusCode) !== -1) { | ||||
|  |             return false; | ||||
|  |         } | ||||
|  |         if (codes.include && codes.include.length && codes.include.indexOf(response.statusCode) === -1) { | ||||
|  |             return false; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         return true; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     function addIndexEntries(key, req) { | ||||
|  |         let groupName = req.apicacheGroup; | ||||
|  | 
 | ||||
|  |         if (groupName) { | ||||
|  |             debug("group detected \"" + groupName + "\""); | ||||
|  |             let group = (index.groups[groupName] = index.groups[groupName] || []); | ||||
|  |             group.unshift(key); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         index.all.unshift(key); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     function filterBlacklistedHeaders(headers) { | ||||
|  |         return Object.keys(headers) | ||||
|  |             .filter(function (key) { | ||||
|  |                 return globalOptions.headerBlacklist.indexOf(key) === -1; | ||||
|  |             }) | ||||
|  |             .reduce(function (acc, header) { | ||||
|  |                 acc[header] = headers[header]; | ||||
|  |                 return acc; | ||||
|  |             }, {}); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     function createCacheObject(status, headers, data, encoding) { | ||||
|  |         return { | ||||
|  |             status: status, | ||||
|  |             headers: filterBlacklistedHeaders(headers), | ||||
|  |             data: data, | ||||
|  |             encoding: encoding, | ||||
|  |             timestamp: new Date().getTime() / 1000, // seconds since epoch.  This is used to properly decrement max-age headers in cached responses.
 | ||||
|  |         }; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     function cacheResponse(key, value, duration) { | ||||
|  |         let redis = globalOptions.redisClient; | ||||
|  |         let expireCallback = globalOptions.events.expire; | ||||
|  | 
 | ||||
|  |         if (redis && redis.connected) { | ||||
|  |             try { | ||||
|  |                 redis.hset(key, "response", JSON.stringify(value)); | ||||
|  |                 redis.hset(key, "duration", duration); | ||||
|  |                 redis.expire(key, duration / 1000, expireCallback || function () {}); | ||||
|  |             } catch (err) { | ||||
|  |                 debug("[apicache] error in redis.hset()"); | ||||
|  |             } | ||||
|  |         } else { | ||||
|  |             memCache.add(key, value, duration, expireCallback); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         // add automatic cache clearing from duration, includes max limit on setTimeout
 | ||||
|  |         timers[key] = setTimeout(function () { | ||||
|  |             instance.clear(key, true); | ||||
|  |         }, Math.min(duration, 2147483647)); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     function accumulateContent(res, content) { | ||||
|  |         if (content) { | ||||
|  |             if (typeof content == "string") { | ||||
|  |                 res._apicache.content = (res._apicache.content || "") + content; | ||||
|  |             } else if (Buffer.isBuffer(content)) { | ||||
|  |                 let oldContent = res._apicache.content; | ||||
|  | 
 | ||||
|  |                 if (typeof oldContent === "string") { | ||||
|  |                     oldContent = !Buffer.from ? new Buffer(oldContent) : Buffer.from(oldContent); | ||||
|  |                 } | ||||
|  | 
 | ||||
|  |                 if (!oldContent) { | ||||
|  |                     oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0); | ||||
|  |                 } | ||||
|  | 
 | ||||
|  |                 res._apicache.content = Buffer.concat( | ||||
|  |                     [oldContent, content], | ||||
|  |                     oldContent.length + content.length | ||||
|  |                 ); | ||||
|  |             } else { | ||||
|  |                 res._apicache.content = content; | ||||
|  |             } | ||||
|  |         } | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) { | ||||
|  |     // monkeypatch res.end to create cache object
 | ||||
|  |         res._apicache = { | ||||
|  |             write: res.write, | ||||
|  |             writeHead: res.writeHead, | ||||
|  |             end: res.end, | ||||
|  |             cacheable: true, | ||||
|  |             content: undefined, | ||||
|  |         }; | ||||
|  | 
 | ||||
|  |         // append header overwrites if applicable
 | ||||
|  |         Object.keys(globalOptions.headers).forEach(function (name) { | ||||
|  |             res.setHeader(name, globalOptions.headers[name]); | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         res.writeHead = function () { | ||||
|  |             // add cache control headers
 | ||||
|  |             if (!globalOptions.headers["cache-control"]) { | ||||
|  |                 if (shouldCacheResponse(req, res, toggle)) { | ||||
|  |                     res.setHeader("cache-control", "max-age=" + (duration / 1000).toFixed(0)); | ||||
|  |                 } else { | ||||
|  |                     res.setHeader("cache-control", "no-cache, no-store, must-revalidate"); | ||||
|  |                 } | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             res._apicache.headers = Object.assign({}, getSafeHeaders(res)); | ||||
|  |             return res._apicache.writeHead.apply(this, arguments); | ||||
|  |         }; | ||||
|  | 
 | ||||
|  |         // patch res.write
 | ||||
|  |         res.write = function (content) { | ||||
|  |             accumulateContent(res, content); | ||||
|  |             return res._apicache.write.apply(this, arguments); | ||||
|  |         }; | ||||
|  | 
 | ||||
|  |         // patch res.end
 | ||||
|  |         res.end = function (content, encoding) { | ||||
|  |             if (shouldCacheResponse(req, res, toggle)) { | ||||
|  |                 accumulateContent(res, content); | ||||
|  | 
 | ||||
|  |                 if (res._apicache.cacheable && res._apicache.content) { | ||||
|  |                     addIndexEntries(key, req); | ||||
|  |                     let headers = res._apicache.headers || getSafeHeaders(res); | ||||
|  |                     let cacheObject = createCacheObject( | ||||
|  |                         res.statusCode, | ||||
|  |                         headers, | ||||
|  |                         res._apicache.content, | ||||
|  |                         encoding | ||||
|  |                     ); | ||||
|  |                     cacheResponse(key, cacheObject, duration); | ||||
|  | 
 | ||||
|  |                     // display log entry
 | ||||
|  |                     let elapsed = new Date() - req.apicacheTimer; | ||||
|  |                     debug("adding cache entry for \"" + key + "\" @ " + strDuration, logDuration(elapsed)); | ||||
|  |                     debug("_apicache.headers: ", res._apicache.headers); | ||||
|  |                     debug("res.getHeaders(): ", getSafeHeaders(res)); | ||||
|  |                     debug("cacheObject: ", cacheObject); | ||||
|  |                 } | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             return res._apicache.end.apply(this, arguments); | ||||
|  |         }; | ||||
|  | 
 | ||||
|  |         next(); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     function sendCachedResponse(request, response, cacheObject, toggle, next, duration) { | ||||
|  |         if (toggle && !toggle(request, response)) { | ||||
|  |             return next(); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         let headers = getSafeHeaders(response); | ||||
|  | 
 | ||||
|  |         // Modified by @louislam, removed Cache-control, since I don't need client side cache!
 | ||||
|  |         // Original Source: https://github.com/kwhitley/apicache/blob/0d5686cc21fad353c6dddee646288c2fca3e4f50/src/apicache.js#L254
 | ||||
|  |         Object.assign(headers, filterBlacklistedHeaders(cacheObject.headers || {})); | ||||
|  | 
 | ||||
|  |         // only embed apicache headers when not in production environment
 | ||||
|  |         if (process.env.NODE_ENV !== "production") { | ||||
|  |             Object.assign(headers, { | ||||
|  |                 "apicache-store": globalOptions.redisClient ? "redis" : "memory", | ||||
|  |                 "apicache-version": "1.6.2-modified", | ||||
|  |             }); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         // unstringify buffers
 | ||||
|  |         let data = cacheObject.data; | ||||
|  |         if (data && data.type === "Buffer") { | ||||
|  |             data = | ||||
|  |         typeof data.data === "number" ? new Buffer.alloc(data.data) : new Buffer.from(data.data); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         // test Etag against If-None-Match for 304
 | ||||
|  |         let cachedEtag = cacheObject.headers.etag; | ||||
|  |         let requestEtag = request.headers["if-none-match"]; | ||||
|  | 
 | ||||
|  |         if (requestEtag && cachedEtag === requestEtag) { | ||||
|  |             response.writeHead(304, headers); | ||||
|  |             return response.end(); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         response.writeHead(cacheObject.status || 200, headers); | ||||
|  | 
 | ||||
|  |         return response.end(data, cacheObject.encoding); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     function syncOptions() { | ||||
|  |         for (let i in middlewareOptions) { | ||||
|  |             Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions); | ||||
|  |         } | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     this.clear = function (target, isAutomatic) { | ||||
|  |         let group = index.groups[target]; | ||||
|  |         let redis = globalOptions.redisClient; | ||||
|  | 
 | ||||
|  |         if (group) { | ||||
|  |             debug("clearing group \"" + target + "\""); | ||||
|  | 
 | ||||
|  |             group.forEach(function (key) { | ||||
|  |                 debug("clearing cached entry for \"" + key + "\""); | ||||
|  |                 clearTimeout(timers[key]); | ||||
|  |                 delete timers[key]; | ||||
|  |                 if (!globalOptions.redisClient) { | ||||
|  |                     memCache.delete(key); | ||||
|  |                 } else { | ||||
|  |                     try { | ||||
|  |                         redis.del(key); | ||||
|  |                     } catch (err) { | ||||
|  |                         console.log("[apicache] error in redis.del(\"" + key + "\")"); | ||||
|  |                     } | ||||
|  |                 } | ||||
|  |                 index.all = index.all.filter(doesntMatch(key)); | ||||
|  |             }); | ||||
|  | 
 | ||||
|  |             delete index.groups[target]; | ||||
|  |         } else if (target) { | ||||
|  |             debug("clearing " + (isAutomatic ? "expired" : "cached") + " entry for \"" + target + "\""); | ||||
|  |             clearTimeout(timers[target]); | ||||
|  |             delete timers[target]; | ||||
|  |             // clear actual cached entry
 | ||||
|  |             if (!redis) { | ||||
|  |                 memCache.delete(target); | ||||
|  |             } else { | ||||
|  |                 try { | ||||
|  |                     redis.del(target); | ||||
|  |                 } catch (err) { | ||||
|  |                     console.log("[apicache] error in redis.del(\"" + target + "\")"); | ||||
|  |                 } | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             // remove from global index
 | ||||
|  |             index.all = index.all.filter(doesntMatch(target)); | ||||
|  | 
 | ||||
|  |             // remove target from each group that it may exist in
 | ||||
|  |             Object.keys(index.groups).forEach(function (groupName) { | ||||
|  |                 index.groups[groupName] = index.groups[groupName].filter(doesntMatch(target)); | ||||
|  | 
 | ||||
|  |                 // delete group if now empty
 | ||||
|  |                 if (!index.groups[groupName].length) { | ||||
|  |                     delete index.groups[groupName]; | ||||
|  |                 } | ||||
|  |             }); | ||||
|  |         } else { | ||||
|  |             debug("clearing entire index"); | ||||
|  | 
 | ||||
|  |             if (!redis) { | ||||
|  |                 memCache.clear(); | ||||
|  |             } else { | ||||
|  |                 // clear redis keys one by one from internal index to prevent clearing non-apicache entries
 | ||||
|  |                 index.all.forEach(function (key) { | ||||
|  |                     clearTimeout(timers[key]); | ||||
|  |                     delete timers[key]; | ||||
|  |                     try { | ||||
|  |                         redis.del(key); | ||||
|  |                     } catch (err) { | ||||
|  |                         console.log("[apicache] error in redis.del(\"" + key + "\")"); | ||||
|  |                     } | ||||
|  |                 }); | ||||
|  |             } | ||||
|  |             this.resetIndex(); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         return this.getIndex(); | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     function parseDuration(duration, defaultDuration) { | ||||
|  |         if (typeof duration === "number") { | ||||
|  |             return duration; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         if (typeof duration === "string") { | ||||
|  |             let split = duration.match(/^([\d\.,]+)\s?(\w+)$/); | ||||
|  | 
 | ||||
|  |             if (split.length === 3) { | ||||
|  |                 let len = parseFloat(split[1]); | ||||
|  |                 let unit = split[2].replace(/s$/i, "").toLowerCase(); | ||||
|  |                 if (unit === "m") { | ||||
|  |                     unit = "ms"; | ||||
|  |                 } | ||||
|  | 
 | ||||
|  |                 return (len || 1) * (t[unit] || 0); | ||||
|  |             } | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         return defaultDuration; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     this.getDuration = function (duration) { | ||||
|  |         return parseDuration(duration, globalOptions.defaultDuration); | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     /** | ||||
|  |    * Return cache performance statistics (hit rate).  Suitable for putting into a route: | ||||
|  |    * <code> | ||||
|  |    * app.get('/api/cache/performance', (req, res) => { | ||||
|  |    *    res.json(apicache.getPerformance()) | ||||
|  |    * }) | ||||
|  |    * </code> | ||||
|  |    */ | ||||
|  |     this.getPerformance = function () { | ||||
|  |         return performanceArray.map(function (p) { | ||||
|  |             return p.report(); | ||||
|  |         }); | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     this.getIndex = function (group) { | ||||
|  |         if (group) { | ||||
|  |             return index.groups[group]; | ||||
|  |         } else { | ||||
|  |             return index; | ||||
|  |         } | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     this.middleware = function cache(strDuration, middlewareToggle, localOptions) { | ||||
|  |         let duration = instance.getDuration(strDuration); | ||||
|  |         let opt = {}; | ||||
|  | 
 | ||||
|  |         middlewareOptions.push({ | ||||
|  |             options: opt, | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         let options = function (localOptions) { | ||||
|  |             if (localOptions) { | ||||
|  |                 middlewareOptions.find(function (middleware) { | ||||
|  |                     return middleware.options === opt; | ||||
|  |                 }).localOptions = localOptions; | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             syncOptions(); | ||||
|  | 
 | ||||
|  |             return opt; | ||||
|  |         }; | ||||
|  | 
 | ||||
|  |         options(localOptions); | ||||
|  | 
 | ||||
|  |         /** | ||||
|  |      * A Function for non tracking performance | ||||
|  |      */ | ||||
|  |         function NOOPCachePerformance() { | ||||
|  |             this.report = this.hit = this.miss = function () {}; // noop;
 | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         /** | ||||
|  |      * A function for tracking and reporting hit rate.  These statistics are returned by the getPerformance() call above. | ||||
|  |      */ | ||||
|  |         function CachePerformance() { | ||||
|  |             /** | ||||
|  |        * Tracks the hit rate for the last 100 requests. | ||||
|  |        * If there have been fewer than 100 requests, the hit rate just considers the requests that have happened. | ||||
|  |        */ | ||||
|  |             this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
 | ||||
|  | 
 | ||||
|  |             /** | ||||
|  |        * Tracks the hit rate for the last 1000 requests. | ||||
|  |        * If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened. | ||||
|  |        */ | ||||
|  |             this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
 | ||||
|  | 
 | ||||
|  |             /** | ||||
|  |        * Tracks the hit rate for the last 10000 requests. | ||||
|  |        * If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened. | ||||
|  |        */ | ||||
|  |             this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
 | ||||
|  | 
 | ||||
|  |             /** | ||||
|  |        * Tracks the hit rate for the last 100000 requests. | ||||
|  |        * If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened. | ||||
|  |        */ | ||||
|  |             this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
 | ||||
|  | 
 | ||||
|  |             /** | ||||
|  |        * The number of calls that have passed through the middleware since the server started. | ||||
|  |        */ | ||||
|  |             this.callCount = 0; | ||||
|  | 
 | ||||
|  |             /** | ||||
|  |        * The total number of hits since the server started | ||||
|  |        */ | ||||
|  |             this.hitCount = 0; | ||||
|  | 
 | ||||
|  |             /** | ||||
|  |        * The key from the last cache hit.  This is useful in identifying which route these statistics apply to. | ||||
|  |        */ | ||||
|  |             this.lastCacheHit = null; | ||||
|  | 
 | ||||
|  |             /** | ||||
|  |        * The key from the last cache miss.  This is useful in identifying which route these statistics apply to. | ||||
|  |        */ | ||||
|  |             this.lastCacheMiss = null; | ||||
|  | 
 | ||||
|  |             /** | ||||
|  |        * Return performance statistics | ||||
|  |        */ | ||||
|  |             this.report = function () { | ||||
|  |                 return { | ||||
|  |                     lastCacheHit: this.lastCacheHit, | ||||
|  |                     lastCacheMiss: this.lastCacheMiss, | ||||
|  |                     callCount: this.callCount, | ||||
|  |                     hitCount: this.hitCount, | ||||
|  |                     missCount: this.callCount - this.hitCount, | ||||
|  |                     hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount, | ||||
|  |                     hitRateLast100: this.hitRate(this.hitsLast100), | ||||
|  |                     hitRateLast1000: this.hitRate(this.hitsLast1000), | ||||
|  |                     hitRateLast10000: this.hitRate(this.hitsLast10000), | ||||
|  |                     hitRateLast100000: this.hitRate(this.hitsLast100000), | ||||
|  |                 }; | ||||
|  |             }; | ||||
|  | 
 | ||||
|  |             /** | ||||
|  |        * Computes a cache hit rate from an array of hits and misses. | ||||
|  |        * @param {Uint8Array} array An array representing hits and misses. | ||||
|  |        * @returns a number between 0 and 1, or null if the array has no hits or misses | ||||
|  |        */ | ||||
|  |             this.hitRate = function (array) { | ||||
|  |                 let hits = 0; | ||||
|  |                 let misses = 0; | ||||
|  |                 for (let i = 0; i < array.length; i++) { | ||||
|  |                     let n8 = array[i]; | ||||
|  |                     for (let j = 0; j < 4; j++) { | ||||
|  |                         switch (n8 & 3) { | ||||
|  |                             case 1: | ||||
|  |                                 hits++; | ||||
|  |                                 break; | ||||
|  |                             case 2: | ||||
|  |                                 misses++; | ||||
|  |                                 break; | ||||
|  |                         } | ||||
|  |                         n8 >>= 2; | ||||
|  |                     } | ||||
|  |                 } | ||||
|  |                 let total = hits + misses; | ||||
|  |                 if (total == 0) { | ||||
|  |                     return null; | ||||
|  |                 } | ||||
|  |                 return hits / total; | ||||
|  |             }; | ||||
|  | 
 | ||||
|  |             /** | ||||
|  |        * Record a hit or miss in the given array.  It will be recorded at a position determined | ||||
|  |        * by the current value of the callCount variable. | ||||
|  |        * @param {Uint8Array} array An array representing hits and misses. | ||||
|  |        * @param {boolean} hit true for a hit, false for a miss | ||||
|  |        * Each element in the array is 8 bits, and encodes 4 hit/miss records. | ||||
|  |        * Each hit or miss is encoded as to bits as follows: | ||||
|  |        * 00 means no hit or miss has been recorded in these bits | ||||
|  |        * 01 encodes a hit | ||||
|  |        * 10 encodes a miss | ||||
|  |        */ | ||||
|  |             this.recordHitInArray = function (array, hit) { | ||||
|  |                 let arrayIndex = ~~(this.callCount / 4) % array.length; | ||||
|  |                 let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
 | ||||
|  |                 let clearMask = ~(3 << bitOffset); | ||||
|  |                 let record = (hit ? 1 : 2) << bitOffset; | ||||
|  |                 array[arrayIndex] = (array[arrayIndex] & clearMask) | record; | ||||
|  |             }; | ||||
|  | 
 | ||||
|  |             /** | ||||
|  |        * Records the hit or miss in the tracking arrays and increments the call count. | ||||
|  |        * @param {boolean} hit true records a hit, false records a miss | ||||
|  |        */ | ||||
|  |             this.recordHit = function (hit) { | ||||
|  |                 this.recordHitInArray(this.hitsLast100, hit); | ||||
|  |                 this.recordHitInArray(this.hitsLast1000, hit); | ||||
|  |                 this.recordHitInArray(this.hitsLast10000, hit); | ||||
|  |                 this.recordHitInArray(this.hitsLast100000, hit); | ||||
|  |                 if (hit) { | ||||
|  |                     this.hitCount++; | ||||
|  |                 } | ||||
|  |                 this.callCount++; | ||||
|  |             }; | ||||
|  | 
 | ||||
|  |             /** | ||||
|  |        * Records a hit event, setting lastCacheMiss to the given key | ||||
|  |        * @param {string} key The key that had the cache hit | ||||
|  |        */ | ||||
|  |             this.hit = function (key) { | ||||
|  |                 this.recordHit(true); | ||||
|  |                 this.lastCacheHit = key; | ||||
|  |             }; | ||||
|  | 
 | ||||
|  |             /** | ||||
|  |        * Records a miss event, setting lastCacheMiss to the given key | ||||
|  |        * @param {string} key The key that had the cache miss | ||||
|  |        */ | ||||
|  |             this.miss = function (key) { | ||||
|  |                 this.recordHit(false); | ||||
|  |                 this.lastCacheMiss = key; | ||||
|  |             }; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         let perf = globalOptions.trackPerformance ? new CachePerformance() : new NOOPCachePerformance(); | ||||
|  | 
 | ||||
|  |         performanceArray.push(perf); | ||||
|  | 
 | ||||
|  |         let cache = function (req, res, next) { | ||||
|  |             function bypass() { | ||||
|  |                 debug("bypass detected, skipping cache."); | ||||
|  |                 return next(); | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             // initial bypass chances
 | ||||
|  |             if (!opt.enabled) { | ||||
|  |                 return bypass(); | ||||
|  |             } | ||||
|  |             if ( | ||||
|  |                 req.headers["x-apicache-bypass"] || | ||||
|  |         req.headers["x-apicache-force-fetch"] || | ||||
|  |         (opt.respectCacheControl && req.headers["cache-control"] == "no-cache") | ||||
|  |             ) { | ||||
|  |                 return bypass(); | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             // REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER
 | ||||
|  |             // if (typeof middlewareToggle === 'function') {
 | ||||
|  |             //   if (!middlewareToggle(req, res)) return bypass()
 | ||||
|  |             // } else if (middlewareToggle !== undefined && !middlewareToggle) {
 | ||||
|  |             //   return bypass()
 | ||||
|  |             // }
 | ||||
|  | 
 | ||||
|  |             // embed timer
 | ||||
|  |             req.apicacheTimer = new Date(); | ||||
|  | 
 | ||||
|  |             // In Express 4.x the url is ambigious based on where a router is mounted.  originalUrl will give the full Url
 | ||||
|  |             let key = req.originalUrl || req.url; | ||||
|  | 
 | ||||
|  |             // Remove querystring from key if jsonp option is enabled
 | ||||
|  |             if (opt.jsonp) { | ||||
|  |                 key = url.parse(key).pathname; | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             // add appendKey (either custom function or response path)
 | ||||
|  |             if (typeof opt.appendKey === "function") { | ||||
|  |                 key += "$$appendKey=" + opt.appendKey(req, res); | ||||
|  |             } else if (opt.appendKey.length > 0) { | ||||
|  |                 let appendKey = req; | ||||
|  | 
 | ||||
|  |                 for (let i = 0; i < opt.appendKey.length; i++) { | ||||
|  |                     appendKey = appendKey[opt.appendKey[i]]; | ||||
|  |                 } | ||||
|  |                 key += "$$appendKey=" + appendKey; | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             // attempt cache hit
 | ||||
|  |             let redis = opt.redisClient; | ||||
|  |             let cached = !redis ? memCache.getValue(key) : null; | ||||
|  | 
 | ||||
|  |             // send if cache hit from memory-cache
 | ||||
|  |             if (cached) { | ||||
|  |                 let elapsed = new Date() - req.apicacheTimer; | ||||
|  |                 debug("sending cached (memory-cache) version of", key, logDuration(elapsed)); | ||||
|  | 
 | ||||
|  |                 perf.hit(key); | ||||
|  |                 return sendCachedResponse(req, res, cached, middlewareToggle, next, duration); | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             // send if cache hit from redis
 | ||||
|  |             if (redis && redis.connected) { | ||||
|  |                 try { | ||||
|  |                     redis.hgetall(key, function (err, obj) { | ||||
|  |                         if (!err && obj && obj.response) { | ||||
|  |                             let elapsed = new Date() - req.apicacheTimer; | ||||
|  |                             debug("sending cached (redis) version of", key, logDuration(elapsed)); | ||||
|  | 
 | ||||
|  |                             perf.hit(key); | ||||
|  |                             return sendCachedResponse( | ||||
|  |                                 req, | ||||
|  |                                 res, | ||||
|  |                                 JSON.parse(obj.response), | ||||
|  |                                 middlewareToggle, | ||||
|  |                                 next, | ||||
|  |                                 duration | ||||
|  |                             ); | ||||
|  |                         } else { | ||||
|  |                             perf.miss(key); | ||||
|  |                             return makeResponseCacheable( | ||||
|  |                                 req, | ||||
|  |                                 res, | ||||
|  |                                 next, | ||||
|  |                                 key, | ||||
|  |                                 duration, | ||||
|  |                                 strDuration, | ||||
|  |                                 middlewareToggle | ||||
|  |                             ); | ||||
|  |                         } | ||||
|  |                     }); | ||||
|  |                 } catch (err) { | ||||
|  |                     // bypass redis on error
 | ||||
|  |                     perf.miss(key); | ||||
|  |                     return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle); | ||||
|  |                 } | ||||
|  |             } else { | ||||
|  |                 perf.miss(key); | ||||
|  |                 return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle); | ||||
|  |             } | ||||
|  |         }; | ||||
|  | 
 | ||||
|  |         cache.options = options; | ||||
|  | 
 | ||||
|  |         return cache; | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     this.options = function (options) { | ||||
|  |         if (options) { | ||||
|  |             Object.assign(globalOptions, options); | ||||
|  |             syncOptions(); | ||||
|  | 
 | ||||
|  |             if ("defaultDuration" in options) { | ||||
|  |                 // Convert the default duration to a number in milliseconds (if needed)
 | ||||
|  |                 globalOptions.defaultDuration = parseDuration(globalOptions.defaultDuration, 3600000); | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             if (globalOptions.trackPerformance) { | ||||
|  |                 debug("WARNING: using trackPerformance flag can cause high memory usage!"); | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             return this; | ||||
|  |         } else { | ||||
|  |             return globalOptions; | ||||
|  |         } | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     this.resetIndex = function () { | ||||
|  |         index = { | ||||
|  |             all: [], | ||||
|  |             groups: {}, | ||||
|  |         }; | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     this.newInstance = function (config) { | ||||
|  |         let instance = new ApiCache(); | ||||
|  | 
 | ||||
|  |         if (config) { | ||||
|  |             instance.options(config); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         return instance; | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     this.clone = function () { | ||||
|  |         return this.newInstance(this.options()); | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     // initialize index
 | ||||
|  |     this.resetIndex(); | ||||
|  | } | ||||
|  | 
 | ||||
|  | module.exports = new ApiCache(); | ||||
| @ -0,0 +1,14 @@ | |||||
|  | const apicache = require("./apicache"); | ||||
|  | 
 | ||||
|  | apicache.options({ | ||||
|  |     headerBlacklist: [ | ||||
|  |         "cache-control" | ||||
|  |     ], | ||||
|  |     headers: { | ||||
|  |         // Disable client side cache, only server side cache.
 | ||||
|  |         // BUG! Not working for the second request
 | ||||
|  |         "cache-control": "no-cache", | ||||
|  |     }, | ||||
|  | }); | ||||
|  | 
 | ||||
|  | module.exports = apicache; | ||||
| @ -0,0 +1,59 @@ | |||||
|  | function MemoryCache() { | ||||
|  |     this.cache = {}; | ||||
|  |     this.size = 0; | ||||
|  | } | ||||
|  | 
 | ||||
|  | MemoryCache.prototype.add = function (key, value, time, timeoutCallback) { | ||||
|  |     let old = this.cache[key]; | ||||
|  |     let instance = this; | ||||
|  | 
 | ||||
|  |     let entry = { | ||||
|  |         value: value, | ||||
|  |         expire: time + Date.now(), | ||||
|  |         timeout: setTimeout(function () { | ||||
|  |             instance.delete(key); | ||||
|  |             return timeoutCallback && typeof timeoutCallback === "function" && timeoutCallback(value, key); | ||||
|  |         }, time) | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     this.cache[key] = entry; | ||||
|  |     this.size = Object.keys(this.cache).length; | ||||
|  | 
 | ||||
|  |     return entry; | ||||
|  | }; | ||||
|  | 
 | ||||
|  | MemoryCache.prototype.delete = function (key) { | ||||
|  |     let entry = this.cache[key]; | ||||
|  | 
 | ||||
|  |     if (entry) { | ||||
|  |         clearTimeout(entry.timeout); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     delete this.cache[key]; | ||||
|  | 
 | ||||
|  |     this.size = Object.keys(this.cache).length; | ||||
|  | 
 | ||||
|  |     return null; | ||||
|  | }; | ||||
|  | 
 | ||||
|  | MemoryCache.prototype.get = function (key) { | ||||
|  |     let entry = this.cache[key]; | ||||
|  | 
 | ||||
|  |     return entry; | ||||
|  | }; | ||||
|  | 
 | ||||
|  | MemoryCache.prototype.getValue = function (key) { | ||||
|  |     let entry = this.get(key); | ||||
|  | 
 | ||||
|  |     return entry && entry.value; | ||||
|  | }; | ||||
|  | 
 | ||||
|  | MemoryCache.prototype.clear = function () { | ||||
|  |     Object.keys(this.cache).forEach(function (key) { | ||||
|  |         this.delete(key); | ||||
|  |     }, this); | ||||
|  | 
 | ||||
|  |     return true; | ||||
|  | }; | ||||
|  | 
 | ||||
|  | module.exports = MemoryCache; | ||||
| @ -0,0 +1,150 @@ | |||||
|  | let express = require("express"); | ||||
|  | const { allowDevAllOrigin, getSettings, setting } = require("../util-server"); | ||||
|  | const { R } = require("redbean-node"); | ||||
|  | const server = require("../server"); | ||||
|  | const apicache = require("../modules/apicache"); | ||||
|  | const Monitor = require("../model/monitor"); | ||||
|  | let router = express.Router(); | ||||
|  | 
 | ||||
|  | let cache = apicache.middleware; | ||||
|  | 
 | ||||
|  | router.get("/api/entry-page", async (_, response) => { | ||||
|  |     allowDevAllOrigin(response); | ||||
|  |     response.json(server.entryPage); | ||||
|  | }); | ||||
|  | 
 | ||||
|  | // Status Page Config
 | ||||
|  | router.get("/api/status-page/config", async (_request, response) => { | ||||
|  |     allowDevAllOrigin(response); | ||||
|  | 
 | ||||
|  |     let config = await getSettings("statusPage"); | ||||
|  | 
 | ||||
|  |     if (! config.statusPageTheme) { | ||||
|  |         config.statusPageTheme = "light"; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     if (! config.statusPagePublished) { | ||||
|  |         config.statusPagePublished = true; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     if (! config.title) { | ||||
|  |         config.title = "Uptime Kuma"; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     response.json(config); | ||||
|  | }); | ||||
|  | 
 | ||||
|  | // Status Page - Get the current Incident
 | ||||
|  | // Can fetch only if published
 | ||||
|  | router.get("/api/status-page/incident", async (_, response) => { | ||||
|  |     allowDevAllOrigin(response); | ||||
|  | 
 | ||||
|  |     try { | ||||
|  |         await checkPublished(); | ||||
|  | 
 | ||||
|  |         let incident = await R.findOne("incident", " pin = 1 AND active = 1"); | ||||
|  | 
 | ||||
|  |         if (incident) { | ||||
|  |             incident = incident.toPublicJSON(); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         response.json({ | ||||
|  |             ok: true, | ||||
|  |             incident, | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |     } catch (error) { | ||||
|  |         send403(response, error.message); | ||||
|  |     } | ||||
|  | }); | ||||
|  | 
 | ||||
|  | // Status Page - Monitor List
 | ||||
|  | // Can fetch only if published
 | ||||
|  | router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => { | ||||
|  |     allowDevAllOrigin(response); | ||||
|  | 
 | ||||
|  |     try { | ||||
|  |         await checkPublished(); | ||||
|  |         const publicGroupList = []; | ||||
|  |         let list = await R.find("group", " public = 1 ORDER BY weight "); | ||||
|  | 
 | ||||
|  |         for (let groupBean of list) { | ||||
|  |             publicGroupList.push(await groupBean.toPublicJSON()); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         response.json(publicGroupList); | ||||
|  | 
 | ||||
|  |     } catch (error) { | ||||
|  |         send403(response, error.message); | ||||
|  |     } | ||||
|  | }); | ||||
|  | 
 | ||||
|  | // Status Page Polling Data
 | ||||
|  | // Can fetch only if published
 | ||||
|  | router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => { | ||||
|  |     allowDevAllOrigin(response); | ||||
|  |     try { | ||||
|  |         await checkPublished(); | ||||
|  | 
 | ||||
|  |         let heartbeatList = {}; | ||||
|  |         let uptimeList = {}; | ||||
|  | 
 | ||||
|  |         let monitorIDList = await R.getCol(` | ||||
|  |             SELECT monitor_group.monitor_id FROM monitor_group, \`group\` | ||||
|  |             WHERE monitor_group.group_id = \`group\`.id
 | ||||
|  |             AND public = 1 | ||||
|  |         `);
 | ||||
|  | 
 | ||||
|  |         for (let monitorID of monitorIDList) { | ||||
|  |             let list = await R.getAll(` | ||||
|  |                     SELECT * FROM heartbeat | ||||
|  |                     WHERE monitor_id = ? | ||||
|  |                     ORDER BY time DESC | ||||
|  |                     LIMIT 50 | ||||
|  |             `, [
 | ||||
|  |                 monitorID, | ||||
|  |             ]); | ||||
|  | 
 | ||||
|  |             list = R.convertToBeans("heartbeat", list); | ||||
|  |             heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON()); | ||||
|  | 
 | ||||
|  |             const type = 24; | ||||
|  |             uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         response.json({ | ||||
|  |             heartbeatList, | ||||
|  |             uptimeList | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |     } catch (error) { | ||||
|  |         send403(response, error.message); | ||||
|  |     } | ||||
|  | }); | ||||
|  | 
 | ||||
|  | async function checkPublished() { | ||||
|  |     if (! await isPublished()) { | ||||
|  |         throw new Error("The status page is not published"); | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | /** | ||||
|  |  * Default is published | ||||
|  |  * @returns {Promise<boolean>} | ||||
|  |  */ | ||||
|  | async function isPublished() { | ||||
|  |     const value = await setting("statusPagePublished"); | ||||
|  |     if (value === null) { | ||||
|  |         return true; | ||||
|  |     } | ||||
|  |     return value; | ||||
|  | } | ||||
|  | 
 | ||||
|  | function send403(res, msg = "") { | ||||
|  |     res.status(403).json({ | ||||
|  |         "status": "fail", | ||||
|  |         "msg": msg, | ||||
|  |     }); | ||||
|  | } | ||||
|  | 
 | ||||
|  | module.exports = router; | ||||
| @ -0,0 +1,161 @@ | |||||
|  | const { R } = require("redbean-node"); | ||||
|  | const { checkLogin, setSettings } = require("../util-server"); | ||||
|  | const dayjs = require("dayjs"); | ||||
|  | const { debug } = require("../../src/util"); | ||||
|  | const ImageDataURI = require("../image-data-uri"); | ||||
|  | const Database = require("../database"); | ||||
|  | const apicache = require("../modules/apicache"); | ||||
|  | 
 | ||||
|  | module.exports.statusPageSocketHandler = (socket) => { | ||||
|  | 
 | ||||
|  |     // Post or edit incident
 | ||||
|  |     socket.on("postIncident", async (incident, callback) => { | ||||
|  |         try { | ||||
|  |             checkLogin(socket); | ||||
|  | 
 | ||||
|  |             await R.exec("UPDATE incident SET pin = 0 "); | ||||
|  | 
 | ||||
|  |             let incidentBean; | ||||
|  | 
 | ||||
|  |             if (incident.id) { | ||||
|  |                 incidentBean = await R.findOne("incident", " id = ?", [ | ||||
|  |                     incident.id | ||||
|  |                 ]); | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             if (incidentBean == null) { | ||||
|  |                 incidentBean = R.dispense("incident"); | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             incidentBean.title = incident.title; | ||||
|  |             incidentBean.content = incident.content; | ||||
|  |             incidentBean.style = incident.style; | ||||
|  |             incidentBean.pin = true; | ||||
|  | 
 | ||||
|  |             if (incident.id) { | ||||
|  |                 incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc()); | ||||
|  |             } else { | ||||
|  |                 incidentBean.createdDate = R.isoDateTime(dayjs.utc()); | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             await R.store(incidentBean); | ||||
|  | 
 | ||||
|  |             callback({ | ||||
|  |                 ok: true, | ||||
|  |                 incident: incidentBean.toPublicJSON(), | ||||
|  |             }); | ||||
|  |         } catch (error) { | ||||
|  |             callback({ | ||||
|  |                 ok: false, | ||||
|  |                 msg: error.message, | ||||
|  |             }); | ||||
|  |         } | ||||
|  |     }); | ||||
|  | 
 | ||||
|  |     socket.on("unpinIncident", async (callback) => { | ||||
|  |         try { | ||||
|  |             checkLogin(socket); | ||||
|  | 
 | ||||
|  |             await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1"); | ||||
|  | 
 | ||||
|  |             callback({ | ||||
|  |                 ok: true, | ||||
|  |             }); | ||||
|  |         } catch (error) { | ||||
|  |             callback({ | ||||
|  |                 ok: false, | ||||
|  |                 msg: error.message, | ||||
|  |             }); | ||||
|  |         } | ||||
|  |     }); | ||||
|  | 
 | ||||
|  |     // Save Status Page
 | ||||
|  |     // imgDataUrl Only Accept PNG!
 | ||||
|  |     socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => { | ||||
|  | 
 | ||||
|  |         try { | ||||
|  |             checkLogin(socket); | ||||
|  | 
 | ||||
|  |             apicache.clear(); | ||||
|  | 
 | ||||
|  |             const header = "data:image/png;base64,"; | ||||
|  | 
 | ||||
|  |             // Check logo format
 | ||||
|  |             // If is image data url, convert to png file
 | ||||
|  |             // Else assume it is a url, nothing to do
 | ||||
|  |             if (imgDataUrl.startsWith("data:")) { | ||||
|  |                 if (! imgDataUrl.startsWith(header)) { | ||||
|  |                     throw new Error("Only allowed PNG logo."); | ||||
|  |                 } | ||||
|  | 
 | ||||
|  |                 // Convert to file
 | ||||
|  |                 await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png"); | ||||
|  |                 config.logo = "/upload/logo.png?t=" + Date.now(); | ||||
|  | 
 | ||||
|  |             } else { | ||||
|  |                 config.icon = imgDataUrl; | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             // Save Config
 | ||||
|  |             await setSettings("statusPage", config); | ||||
|  | 
 | ||||
|  |             // Save Public Group List
 | ||||
|  |             const groupIDList = []; | ||||
|  |             let groupOrder = 1; | ||||
|  | 
 | ||||
|  |             for (let group of publicGroupList) { | ||||
|  |                 let groupBean; | ||||
|  |                 if (group.id) { | ||||
|  |                     groupBean = await R.findOne("group", " id = ? AND public = 1 ", [ | ||||
|  |                         group.id | ||||
|  |                     ]); | ||||
|  |                 } else { | ||||
|  |                     groupBean = R.dispense("group"); | ||||
|  |                 } | ||||
|  | 
 | ||||
|  |                 groupBean.name = group.name; | ||||
|  |                 groupBean.public = true; | ||||
|  |                 groupBean.weight = groupOrder++; | ||||
|  | 
 | ||||
|  |                 await R.store(groupBean); | ||||
|  | 
 | ||||
|  |                 await R.exec("DELETE FROM monitor_group WHERE group_id = ? ", [ | ||||
|  |                     groupBean.id | ||||
|  |                 ]); | ||||
|  | 
 | ||||
|  |                 let monitorOrder = 1; | ||||
|  |                 console.log(group.monitorList); | ||||
|  | 
 | ||||
|  |                 for (let monitor of group.monitorList) { | ||||
|  |                     let relationBean = R.dispense("monitor_group"); | ||||
|  |                     relationBean.weight = monitorOrder++; | ||||
|  |                     relationBean.group_id = groupBean.id; | ||||
|  |                     relationBean.monitor_id = monitor.id; | ||||
|  |                     await R.store(relationBean); | ||||
|  |                 } | ||||
|  | 
 | ||||
|  |                 groupIDList.push(groupBean.id); | ||||
|  |                 group.id = groupBean.id; | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             // Delete groups that not in the list
 | ||||
|  |             debug("Delete groups that not in the list"); | ||||
|  |             const slots = groupIDList.map(() => "?").join(","); | ||||
|  |             await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList); | ||||
|  | 
 | ||||
|  |             callback({ | ||||
|  |                 ok: true, | ||||
|  |                 publicGroupList, | ||||
|  |             }); | ||||
|  | 
 | ||||
|  |         } catch (error) { | ||||
|  |             console.log(error); | ||||
|  | 
 | ||||
|  |             callback({ | ||||
|  |                 ok: false, | ||||
|  |                 msg: error.message, | ||||
|  |             }); | ||||
|  |         } | ||||
|  |     }); | ||||
|  | 
 | ||||
|  | }; | ||||
| @ -0,0 +1,144 @@ | |||||
|  | <template> | ||||
|  |     <!-- Group List --> | ||||
|  |     <Draggable | ||||
|  |         v-model="$root.publicGroupList" | ||||
|  |         :disabled="!editMode" | ||||
|  |         item-key="id" | ||||
|  |         :animation="100" | ||||
|  |     > | ||||
|  |         <template #item="group"> | ||||
|  |             <div class="mb-5 "> | ||||
|  |                 <!-- Group Title --> | ||||
|  |                 <h2 class="group-title"> | ||||
|  |                     <font-awesome-icon v-if="editMode && showGroupDrag" icon="arrows-alt-v" class="action drag me-3" /> | ||||
|  |                     <font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeGroup(group.index)" /> | ||||
|  |                     <Editable v-model="group.element.name" :contenteditable="editMode" tag="span" /> | ||||
|  |                 </h2> | ||||
|  | 
 | ||||
|  |                 <div class="shadow-box monitor-list mt-4 position-relative"> | ||||
|  |                     <div v-if="group.element.monitorList.length === 0" class="text-center no-monitor-msg"> | ||||
|  |                         {{ $t("No Monitors") }} | ||||
|  |                     </div> | ||||
|  | 
 | ||||
|  |                     <!-- Monitor List --> | ||||
|  |                     <!-- animation is not working, no idea why --> | ||||
|  |                     <Draggable | ||||
|  |                         v-model="group.element.monitorList" | ||||
|  |                         class="monitor-list" | ||||
|  |                         group="same-group" | ||||
|  |                         :disabled="!editMode" | ||||
|  |                         :animation="100" | ||||
|  |                         item-key="id" | ||||
|  |                     > | ||||
|  |                         <template #item="monitor"> | ||||
|  |                             <div class="item"> | ||||
|  |                                 <div class="row"> | ||||
|  |                                     <div class="col-9 col-md-8 small-padding"> | ||||
|  |                                         <div class="info"> | ||||
|  |                                             <font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" /> | ||||
|  |                                             <font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" /> | ||||
|  | 
 | ||||
|  |                                             <Uptime :monitor="monitor.element" type="24" :pill="true" /> | ||||
|  |                                             {{ monitor.element.name }} | ||||
|  |                                         </div> | ||||
|  |                                     </div> | ||||
|  |                                     <div :key="$root.userHeartbeatBar" class="col-3 col-md-4"> | ||||
|  |                                         <HeartbeatBar size="small" :monitor-id="monitor.element.id" /> | ||||
|  |                                     </div> | ||||
|  |                                 </div> | ||||
|  |                             </div> | ||||
|  |                         </template> | ||||
|  |                     </Draggable> | ||||
|  |                 </div> | ||||
|  |             </div> | ||||
|  |         </template> | ||||
|  |     </Draggable> | ||||
|  | </template> | ||||
|  | 
 | ||||
|  | <script> | ||||
|  | import Draggable from "vuedraggable"; | ||||
|  | import HeartbeatBar from "./HeartbeatBar.vue"; | ||||
|  | import Uptime from "./Uptime.vue"; | ||||
|  | 
 | ||||
|  | export default { | ||||
|  |     components: { | ||||
|  |         Draggable, | ||||
|  |         HeartbeatBar, | ||||
|  |         Uptime, | ||||
|  |     }, | ||||
|  |     props: { | ||||
|  |         editMode: { | ||||
|  |             type: Boolean, | ||||
|  |             required: true, | ||||
|  |         }, | ||||
|  |     }, | ||||
|  |     data() { | ||||
|  |         return { | ||||
|  | 
 | ||||
|  |         }; | ||||
|  |     }, | ||||
|  |     computed: { | ||||
|  |         showGroupDrag() { | ||||
|  |             return (this.$root.publicGroupList.length >= 2); | ||||
|  |         } | ||||
|  |     }, | ||||
|  |     created() { | ||||
|  | 
 | ||||
|  |     }, | ||||
|  |     methods: { | ||||
|  |         removeGroup(index) { | ||||
|  |             this.$root.publicGroupList.splice(index, 1); | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         removeMonitor(groupIndex, index) { | ||||
|  |             this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1); | ||||
|  |         }, | ||||
|  |     } | ||||
|  | }; | ||||
|  | </script> | ||||
|  | 
 | ||||
|  | <style lang="scss" scoped> | ||||
|  | @import "../assets/vars"; | ||||
|  | 
 | ||||
|  | .no-monitor-msg { | ||||
|  |     position: absolute; | ||||
|  |     width: 100%; | ||||
|  |     top: 20px; | ||||
|  |     left: 0; | ||||
|  | } | ||||
|  | 
 | ||||
|  | .monitor-list { | ||||
|  |     min-height: 46px; | ||||
|  | } | ||||
|  | 
 | ||||
|  | .flip-list-move { | ||||
|  |     transition: transform 0.5s; | ||||
|  | } | ||||
|  | 
 | ||||
|  | .no-move { | ||||
|  |     transition: transform 0s; | ||||
|  | } | ||||
|  | 
 | ||||
|  | .drag { | ||||
|  |     color: #bbb; | ||||
|  |     cursor: grab; | ||||
|  | } | ||||
|  | 
 | ||||
|  | .remove { | ||||
|  |     color: $danger; | ||||
|  | } | ||||
|  | 
 | ||||
|  | .group-title { | ||||
|  |     span { | ||||
|  |         display: inline-block; | ||||
|  |         min-width: 15px; | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | .mobile { | ||||
|  |     .item { | ||||
|  |         padding: 13px 0 10px 0; | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | </style> | ||||
| @ -0,0 +1,40 @@ | |||||
|  | import axios from "axios"; | ||||
|  | 
 | ||||
|  | const env = process.env.NODE_ENV || "production"; | ||||
|  | 
 | ||||
|  | // change the axios base url for development
 | ||||
|  | if (env === "development" || localStorage.dev === "dev") { | ||||
|  |     axios.defaults.baseURL = location.protocol + "//" + location.hostname + ":3001"; | ||||
|  | } | ||||
|  | 
 | ||||
|  | export default { | ||||
|  |     data() { | ||||
|  |         return { | ||||
|  |             publicGroupList: [], | ||||
|  |         }; | ||||
|  |     }, | ||||
|  |     computed: { | ||||
|  |         publicMonitorList() { | ||||
|  |             let result = {}; | ||||
|  | 
 | ||||
|  |             for (let group of this.publicGroupList) { | ||||
|  |                 for (let monitor of group.monitorList) { | ||||
|  |                     result[monitor.id] = monitor; | ||||
|  |                 } | ||||
|  |             } | ||||
|  |             return result; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         publicLastHeartbeatList() { | ||||
|  |             let result = {}; | ||||
|  | 
 | ||||
|  |             for (let monitorID in this.publicMonitorList) { | ||||
|  |                 if (this.lastHeartbeatList[monitorID]) { | ||||
|  |                     result[monitorID] = this.lastHeartbeatList[monitorID]; | ||||
|  |                 } | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             return result; | ||||
|  |         }, | ||||
|  |     } | ||||
|  | }; | ||||
| @ -0,0 +1,20 @@ | |||||
|  | <template> | ||||
|  |     <div></div> | ||||
|  | </template> | ||||
|  | 
 | ||||
|  | <script> | ||||
|  | import axios from "axios"; | ||||
|  | 
 | ||||
|  | export default { | ||||
|  |     async mounted() { | ||||
|  |         let entryPage = (await axios.get("/api/entry-page")).data; | ||||
|  | 
 | ||||
|  |         if (entryPage === "statusPage") { | ||||
|  |             this.$router.push("/status-page"); | ||||
|  |         } else { | ||||
|  |             this.$router.push("/dashboard"); | ||||
|  |         } | ||||
|  |     }, | ||||
|  | 
 | ||||
|  | }; | ||||
|  | </script> | ||||
| @ -0,0 +1,647 @@ | |||||
|  | <template> | ||||
|  |     <div v-if="loadedTheme" class="container mt-3"> | ||||
|  |         <!-- Logo & Title --> | ||||
|  |         <h1 class="mb-4"> | ||||
|  |             <!-- Logo --> | ||||
|  |             <span class="logo-wrapper" @click="showImageCropUploadMethod"> | ||||
|  |                 <img :src="logoURL" alt class="logo me-2" :class="logoClass" /> | ||||
|  |                 <font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" /> | ||||
|  |             </span> | ||||
|  | 
 | ||||
|  |             <!-- Uploader --> | ||||
|  |             <!--    url="/api/status-page/upload-logo" --> | ||||
|  |             <ImageCropUpload v-model="showImageCropUpload" | ||||
|  |                              field="img" | ||||
|  |                              :width="128" | ||||
|  |                              :height="128" | ||||
|  |                              :langType="$i18n.locale" | ||||
|  |                              img-format="png" | ||||
|  |                              :noCircle="true" | ||||
|  |                              :noSquare="false" | ||||
|  |                              @crop-success="cropSuccess" | ||||
|  |             /> | ||||
|  | 
 | ||||
|  |             <!-- Title --> | ||||
|  |             <Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" /> | ||||
|  |         </h1> | ||||
|  | 
 | ||||
|  |         <!-- Admin functions --> | ||||
|  |         <div v-if="hasToken" class="mb-4"> | ||||
|  |             <div v-if="!enableEditMode"> | ||||
|  |                 <button class="btn btn-info me-2" @click="edit"> | ||||
|  |                     <font-awesome-icon icon="edit" /> | ||||
|  |                     Edit Status Page | ||||
|  |                 </button> | ||||
|  | 
 | ||||
|  |                 <a href="/dashboard" class="btn btn-info"> | ||||
|  |                     <font-awesome-icon icon="tachometer-alt" /> | ||||
|  |                     Go to Dashboard | ||||
|  |                 </a> | ||||
|  |             </div> | ||||
|  | 
 | ||||
|  |             <div v-else> | ||||
|  |                 <button class="btn btn-success me-2" @click="save"> | ||||
|  |                     <font-awesome-icon icon="save" /> | ||||
|  |                     {{ $t("Save") }} | ||||
|  |                 </button> | ||||
|  | 
 | ||||
|  |                 <button class="btn btn-danger me-2" @click="discard"> | ||||
|  |                     <font-awesome-icon icon="save" /> | ||||
|  |                     {{ $t("Discard") }} | ||||
|  |                 </button> | ||||
|  | 
 | ||||
|  |                 <button class="btn btn-primary btn-add-group me-2" @click="createIncident"> | ||||
|  |                     <font-awesome-icon icon="bullhorn" /> | ||||
|  |                     {{ $t("Create Incident") }} | ||||
|  |                 </button> | ||||
|  | 
 | ||||
|  |                 <!-- | ||||
|  |                 <button v-if="isPublished" class="btn btn-light me-2" @click=""> | ||||
|  |                     <font-awesome-icon icon="save" /> | ||||
|  |                     {{ $t("Unpublish") }} | ||||
|  |                 </button> | ||||
|  | 
 | ||||
|  |                 <button v-if="!isPublished" class="btn btn-info me-2" @click=""> | ||||
|  |                     <font-awesome-icon icon="save" /> | ||||
|  |                     {{ $t("Publish") }} | ||||
|  |                 </button>--> | ||||
|  | 
 | ||||
|  |                 <!-- Set Default Language --> | ||||
|  |                 <!-- Set theme --> | ||||
|  |                 <button v-if="theme == 'dark'" class="btn btn-light me-2" @click="changeTheme('light')"> | ||||
|  |                     <font-awesome-icon icon="save" /> | ||||
|  |                     {{ $t("Switch to Light Theme") }} | ||||
|  |                 </button> | ||||
|  | 
 | ||||
|  |                 <button v-if="theme == 'light'" class="btn btn-dark me-2" @click="changeTheme('dark')"> | ||||
|  |                     <font-awesome-icon icon="save" /> | ||||
|  |                     {{ $t("Switch to Dark Theme") }} | ||||
|  |                 </button> | ||||
|  |             </div> | ||||
|  |         </div> | ||||
|  | 
 | ||||
|  |         <!-- Incident --> | ||||
|  |         <div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass"> | ||||
|  |             <strong v-if="editIncidentMode">{{ $t("Title") }}:</strong> | ||||
|  |             <Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" /> | ||||
|  | 
 | ||||
|  |             <strong v-if="editIncidentMode">{{ $t("Content") }}:</strong> | ||||
|  |             <Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" /> | ||||
|  | 
 | ||||
|  |             <!-- Incident Date --> | ||||
|  |             <div class="date mt-3"> | ||||
|  |                 Created: {{ incident.createdDate }} ({{ createdDateFromNow }})<br /> | ||||
|  |                 <span v-if="incident.lastUpdatedDate"> | ||||
|  |                     Last Updated: {{ incident.lastUpdatedDate }} ({{ lastUpdatedDateFromNow }}) | ||||
|  |                 </span> | ||||
|  |             </div> | ||||
|  | 
 | ||||
|  |             <div v-if="editMode" class="mt-3"> | ||||
|  |                 <button v-if="editIncidentMode" class="btn btn-light me-2" @click="postIncident"> | ||||
|  |                     <font-awesome-icon icon="bullhorn" /> | ||||
|  |                     {{ $t("Post") }} | ||||
|  |                 </button> | ||||
|  | 
 | ||||
|  |                 <button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="editIncident"> | ||||
|  |                     <font-awesome-icon icon="edit" /> | ||||
|  |                     {{ $t("Edit") }} | ||||
|  |                 </button> | ||||
|  | 
 | ||||
|  |                 <button v-if="editIncidentMode" class="btn btn-light me-2" @click="cancelIncident"> | ||||
|  |                     <font-awesome-icon icon="times" /> | ||||
|  |                     {{ $t("Cancel") }} | ||||
|  |                 </button> | ||||
|  | 
 | ||||
|  |                 <div v-if="editIncidentMode" class="dropdown d-inline-block me-2"> | ||||
|  |                     <button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> | ||||
|  |                         Style: {{ incident.style }} | ||||
|  |                     </button> | ||||
|  |                     <ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1"> | ||||
|  |                         <li><a class="dropdown-item" href="#" @click="incident.style = 'info'">info</a></li> | ||||
|  |                         <li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">warning</a></li> | ||||
|  |                         <li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">danger</a></li> | ||||
|  |                         <li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">primary</a></li> | ||||
|  |                         <li><a class="dropdown-item" href="#" @click="incident.style = 'light'">light</a></li> | ||||
|  |                         <li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">dark</a></li> | ||||
|  |                     </ul> | ||||
|  |                 </div> | ||||
|  | 
 | ||||
|  |                 <button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="unpinIncident"> | ||||
|  |                     <font-awesome-icon icon="unlink" /> | ||||
|  |                     {{ $t("Unpin") }} | ||||
|  |                 </button> | ||||
|  |             </div> | ||||
|  |         </div> | ||||
|  | 
 | ||||
|  |         <!-- Overall Status --> | ||||
|  |         <div class="shadow-box list  p-4 overall-status mb-4"> | ||||
|  |             <div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData"> | ||||
|  |                 <font-awesome-icon icon="question-circle" class="ok" /> | ||||
|  |                 No Services | ||||
|  |             </div> | ||||
|  | 
 | ||||
|  |             <template v-else> | ||||
|  |                 <div v-if="allUp"> | ||||
|  |                     <font-awesome-icon icon="check-circle" class="ok" /> | ||||
|  |                     All Systems Operational | ||||
|  |                 </div> | ||||
|  | 
 | ||||
|  |                 <div v-else-if="partialDown"> | ||||
|  |                     <font-awesome-icon icon="exclamation-circle" class="warning" /> | ||||
|  |                     Partially Degraded Service | ||||
|  |                 </div> | ||||
|  | 
 | ||||
|  |                 <div v-else-if="allDown"> | ||||
|  |                     <font-awesome-icon icon="times-circle" class="danger" /> | ||||
|  |                     Degraded Service | ||||
|  |                 </div> | ||||
|  | 
 | ||||
|  |                 <div v-else> | ||||
|  |                     <font-awesome-icon icon="question-circle" style="color: #efefef" /> | ||||
|  |                 </div> | ||||
|  |             </template> | ||||
|  |         </div> | ||||
|  | 
 | ||||
|  |         <!-- Description --> | ||||
|  |         <strong v-if="editMode">{{ $t("Description") }}:</strong> | ||||
|  |         <Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" /> | ||||
|  | 
 | ||||
|  |         <div v-if="editMode" class="mb-4"> | ||||
|  |             <div> | ||||
|  |                 <button class="btn btn-primary btn-add-group me-2" @click="addGroup"> | ||||
|  |                     <font-awesome-icon icon="plus" /> | ||||
|  |                     Add Group | ||||
|  |                 </button> | ||||
|  |             </div> | ||||
|  | 
 | ||||
|  |             <div class="mt-3"> | ||||
|  |                 <label>Add a monitor:</label> | ||||
|  |                 <select v-model="selectedMonitor" class="form-control"> | ||||
|  |                     <option v-for="monitor in allMonitorList" :key="monitor.id" :value="monitor">{{ monitor.name }}</option> | ||||
|  |                 </select> | ||||
|  |             </div> | ||||
|  |         </div> | ||||
|  | 
 | ||||
|  |         <div class="mb-4"> | ||||
|  |             <div v-if="$root.publicGroupList.length === 0 && loadedData" class="text-center"> | ||||
|  |                 👀 Nothing here, please add a group or a monitor. | ||||
|  |             </div> | ||||
|  | 
 | ||||
|  |             <PublicGroupList :edit-mode="enableEditMode" /> | ||||
|  |         </div> | ||||
|  | 
 | ||||
|  |         <footer class="mt-5 mb-4"> | ||||
|  |             Powered by <a target="_blank" href="https://github.com/louislam/uptime-kuma">Uptime Kuma</a> | ||||
|  |         </footer> | ||||
|  |     </div> | ||||
|  | </template> | ||||
|  | 
 | ||||
|  | <script> | ||||
|  | import axios from "axios"; | ||||
|  | import PublicGroupList from "../components/PublicGroupList.vue"; | ||||
|  | import ImageCropUpload from "vue-image-crop-upload"; | ||||
|  | import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts"; | ||||
|  | import { useToast } from "vue-toastification"; | ||||
|  | import dayjs from "dayjs"; | ||||
|  | const toast = useToast(); | ||||
|  | 
 | ||||
|  | const leavePageMsg = "Do you really want to leave? you have unsaved changes!"; | ||||
|  | 
 | ||||
|  | let feedInterval; | ||||
|  | 
 | ||||
|  | export default { | ||||
|  |     components: { | ||||
|  |         PublicGroupList, | ||||
|  |         ImageCropUpload | ||||
|  |     }, | ||||
|  | 
 | ||||
|  |     // Leave Page for vue route change | ||||
|  |     beforeRouteLeave(to, from, next) { | ||||
|  |         if (this.editMode) { | ||||
|  |             const answer = window.confirm(leavePageMsg); | ||||
|  |             if (answer) { | ||||
|  |                 next(); | ||||
|  |             } else { | ||||
|  |                 next(false); | ||||
|  |             } | ||||
|  |         } | ||||
|  |         next(); | ||||
|  |     }, | ||||
|  | 
 | ||||
|  |     data() { | ||||
|  |         return { | ||||
|  |             enableEditMode: false, | ||||
|  |             enableEditIncidentMode: false, | ||||
|  |             hasToken: false, | ||||
|  |             config: {}, | ||||
|  |             selectedMonitor: null, | ||||
|  |             incident: null, | ||||
|  |             previousIncident: null, | ||||
|  |             showImageCropUpload: false, | ||||
|  |             imgDataUrl: "/icon.svg", | ||||
|  |             loadedTheme: false, | ||||
|  |             loadedData: false, | ||||
|  |             baseURL: "", | ||||
|  |         }; | ||||
|  |     }, | ||||
|  |     computed: { | ||||
|  | 
 | ||||
|  |         logoURL() { | ||||
|  |             if (this.imgDataUrl.startsWith("data:")) { | ||||
|  |                 return this.imgDataUrl; | ||||
|  |             } else { | ||||
|  |                 return this.baseURL + this.imgDataUrl; | ||||
|  |             } | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         /** | ||||
|  |          * If the monitor is added to public list, which will not be in this list. | ||||
|  |          */ | ||||
|  |         allMonitorList() { | ||||
|  |             let result = []; | ||||
|  | 
 | ||||
|  |             for (let id in this.$root.monitorList) { | ||||
|  |                 if (this.$root.monitorList[id] && ! (id in this.$root.publicMonitorList)) { | ||||
|  |                     let monitor = this.$root.monitorList[id]; | ||||
|  |                     result.push(monitor); | ||||
|  |                 } | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             return result; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         editMode() { | ||||
|  |             return this.enableEditMode && this.$root.socket.connected; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         editIncidentMode() { | ||||
|  |             return this.enableEditIncidentMode; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         isPublished() { | ||||
|  |             return this.config.statusPagePublished; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         theme() { | ||||
|  |             return this.config.statusPageTheme; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         logoClass() { | ||||
|  |             if (this.editMode) { | ||||
|  |                 return { | ||||
|  |                     "edit-mode": true, | ||||
|  |                 }; | ||||
|  |             } | ||||
|  |             return {}; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         incidentClass() { | ||||
|  |             return "bg-" + this.incident.style; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         overallStatus() { | ||||
|  | 
 | ||||
|  |             if (Object.keys(this.$root.publicLastHeartbeatList).length === 0) { | ||||
|  |                 return -1; | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             let status = STATUS_PAGE_ALL_UP; | ||||
|  |             let hasUp = false; | ||||
|  | 
 | ||||
|  |             for (let id in this.$root.publicLastHeartbeatList) { | ||||
|  |                 let beat = this.$root.publicLastHeartbeatList[id]; | ||||
|  | 
 | ||||
|  |                 if (beat.status === UP) { | ||||
|  |                     hasUp = true; | ||||
|  |                 } else { | ||||
|  |                     status = STATUS_PAGE_PARTIAL_DOWN; | ||||
|  |                 } | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             if (! hasUp) { | ||||
|  |                 status = STATUS_PAGE_ALL_DOWN; | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             return status; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         allUp() { | ||||
|  |             return this.overallStatus === STATUS_PAGE_ALL_UP; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         partialDown() { | ||||
|  |             return this.overallStatus === STATUS_PAGE_PARTIAL_DOWN; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         allDown() { | ||||
|  |             return this.overallStatus === STATUS_PAGE_ALL_DOWN; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         createdDateFromNow() { | ||||
|  |             return dayjs.utc(this.incident.createdDate).fromNow(); | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         lastUpdatedDateFromNow() { | ||||
|  |             return dayjs.utc(this.incident. lastUpdatedDate).fromNow(); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |     }, | ||||
|  |     watch: { | ||||
|  | 
 | ||||
|  |         /** | ||||
|  |          * Selected a monitor and add to the list. | ||||
|  |          */ | ||||
|  |         selectedMonitor(monitor) { | ||||
|  |             if (monitor) { | ||||
|  |                 if (this.$root.publicGroupList.length === 0) { | ||||
|  |                     this.addGroup(); | ||||
|  |                 } | ||||
|  | 
 | ||||
|  |                 const firstGroup = this.$root.publicGroupList[0]; | ||||
|  | 
 | ||||
|  |                 firstGroup.monitorList.push(monitor); | ||||
|  |                 this.selectedMonitor = null; | ||||
|  |             } | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         // Set Theme | ||||
|  |         "config.statusPageTheme"() { | ||||
|  |             this.$root.statusPageTheme = this.config.statusPageTheme; | ||||
|  |             this.loadedTheme = true; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         "config.title"(title) { | ||||
|  |             document.title = title; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |     }, | ||||
|  |     async created() { | ||||
|  |         this.hasToken = ("token" in localStorage); | ||||
|  | 
 | ||||
|  |         // Browser change page | ||||
|  |         // https://stackoverflow.com/questions/7317273/warn-user-before-leaving-web-page-with-unsaved-changes | ||||
|  |         window.addEventListener("beforeunload", (e) => { | ||||
|  |             if (this.editMode) { | ||||
|  |                 (e || window.event).returnValue = leavePageMsg; | ||||
|  |                 return leavePageMsg; | ||||
|  |             } else { | ||||
|  |                 return null; | ||||
|  |             } | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         // Special handle for dev | ||||
|  |         const env = process.env.NODE_ENV; | ||||
|  |         if (env === "development" || localStorage.dev === "dev") { | ||||
|  |             this.baseURL = location.protocol + "//" + location.hostname + ":3001"; | ||||
|  |         } | ||||
|  |     }, | ||||
|  |     async mounted() { | ||||
|  |         axios.get("/api/status-page/config").then((res) => { | ||||
|  |             this.config = res.data; | ||||
|  | 
 | ||||
|  |             if (this.config.logo) { | ||||
|  |                 this.imgDataUrl = this.config.logo; | ||||
|  |             } | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         axios.get("/api/status-page/incident").then((res) => { | ||||
|  |             if (res.data.ok) { | ||||
|  |                 this.incident = res.data.incident; | ||||
|  |             } | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         axios.get("/api/status-page/monitor-list").then((res) => { | ||||
|  |             this.$root.publicGroupList = res.data; | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         // 5mins a loop | ||||
|  |         this.updateHeartbeatList(); | ||||
|  |         feedInterval = setInterval(() => { | ||||
|  |             this.updateHeartbeatList(); | ||||
|  |         }, (300 + 10) * 1000); | ||||
|  |     }, | ||||
|  |     methods: { | ||||
|  | 
 | ||||
|  |         updateHeartbeatList() { | ||||
|  |             // If editMode, it will use the data from websocket. | ||||
|  |             if (! this.editMode) { | ||||
|  |                 axios.get("/api/status-page/heartbeat").then((res) => { | ||||
|  |                     this.$root.heartbeatList = res.data.heartbeatList; | ||||
|  |                     this.$root.uptimeList = res.data.uptimeList; | ||||
|  |                     this.loadedData = true; | ||||
|  |                 }); | ||||
|  |             } | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         edit() { | ||||
|  |             this.$root.initSocketIO(true); | ||||
|  |             this.enableEditMode = true; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         save() { | ||||
|  |             this.$root.getSocket().emit("saveStatusPage", this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => { | ||||
|  |                 if (res.ok) { | ||||
|  |                     this.enableEditMode = false; | ||||
|  |                     this.$root.publicGroupList = res.publicGroupList; | ||||
|  |                     location.reload(); | ||||
|  |                 } else { | ||||
|  |                     toast.error(res.msg); | ||||
|  |                 } | ||||
|  |             }); | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         monitorSelectorLabel(monitor) { | ||||
|  |             return `${monitor.name}`; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         addGroup() { | ||||
|  |             let groupName = "Untitled Group"; | ||||
|  | 
 | ||||
|  |             if (this.$root.publicGroupList.length === 0) { | ||||
|  |                 groupName = "Services"; | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             this.$root.publicGroupList.push({ | ||||
|  |                 name: groupName, | ||||
|  |                 monitorList: [], | ||||
|  |             }); | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         discard() { | ||||
|  |             location.reload(); | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         changeTheme(name) { | ||||
|  |             this.config.statusPageTheme = name; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         /** | ||||
|  |          * Crop Success | ||||
|  |          */ | ||||
|  |         cropSuccess(imgDataUrl) { | ||||
|  |             this.imgDataUrl = imgDataUrl; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         showImageCropUploadMethod() { | ||||
|  |             if (this.editMode) { | ||||
|  |                 this.showImageCropUpload = true; | ||||
|  |             } | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         createIncident() { | ||||
|  |             this.enableEditIncidentMode = true; | ||||
|  | 
 | ||||
|  |             if (this.incident) { | ||||
|  |                 this.previousIncident = this.incident; | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             this.incident = { | ||||
|  |                 title: "", | ||||
|  |                 content: "", | ||||
|  |                 style: "primary", | ||||
|  |             }; | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         postIncident() { | ||||
|  |             if (this.incident.title == "" || this.incident.content == "") { | ||||
|  |                 toast.error("Please input title and content."); | ||||
|  |                 return; | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             this.$root.getSocket().emit("postIncident", this.incident, (res) => { | ||||
|  | 
 | ||||
|  |                 if (res.ok) { | ||||
|  |                     this.enableEditIncidentMode = false; | ||||
|  |                     this.incident = res.incident; | ||||
|  |                 } else { | ||||
|  |                     toast.error(res.msg); | ||||
|  |                 } | ||||
|  | 
 | ||||
|  |             }); | ||||
|  | 
 | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         /** | ||||
|  |          * Click Edit Button | ||||
|  |          */ | ||||
|  |         editIncident() { | ||||
|  |             this.enableEditIncidentMode = true; | ||||
|  |             this.previousIncident = Object.assign({}, this.incident); | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         cancelIncident() { | ||||
|  |             this.enableEditIncidentMode = false; | ||||
|  | 
 | ||||
|  |             if (this.previousIncident) { | ||||
|  |                 this.incident = this.previousIncident; | ||||
|  |                 this.previousIncident = null; | ||||
|  |             } | ||||
|  |         }, | ||||
|  | 
 | ||||
|  |         unpinIncident() { | ||||
|  |             this.$root.getSocket().emit("unpinIncident", () => { | ||||
|  |                 this.incident = null; | ||||
|  |             }); | ||||
|  |         } | ||||
|  |     } | ||||
|  | }; | ||||
|  | </script> | ||||
|  | 
 | ||||
|  | <style lang="scss" scoped> | ||||
|  | @import "../assets/vars.scss"; | ||||
|  | 
 | ||||
|  | .overall-status { | ||||
|  |     font-weight: bold; | ||||
|  |     font-size: 25px; | ||||
|  | 
 | ||||
|  |     .ok { | ||||
|  |         color: $primary; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     .warning { | ||||
|  |         color: $warning; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     .danger { | ||||
|  |         color: $danger; | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | h1 { | ||||
|  |     font-size: 30px; | ||||
|  | 
 | ||||
|  |     img { | ||||
|  |         vertical-align: middle; | ||||
|  |         height: 60px; | ||||
|  |         width: 60px; | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | footer { | ||||
|  |     text-align: center; | ||||
|  |     font-size: 14px; | ||||
|  | } | ||||
|  | 
 | ||||
|  | .description span { | ||||
|  |     min-width: 50px; | ||||
|  | } | ||||
|  | 
 | ||||
|  | .logo-wrapper { | ||||
|  |     display: inline-block; | ||||
|  |     position: relative; | ||||
|  | 
 | ||||
|  |     &:hover { | ||||
|  |         .icon-upload { | ||||
|  |             transform: scale(1.2); | ||||
|  |         } | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     .icon-upload { | ||||
|  |         transition: all $easing-in 0.2s; | ||||
|  |         position: absolute; | ||||
|  |         bottom: 6px; | ||||
|  |         font-size: 20px; | ||||
|  |         left: -14px; | ||||
|  |         background-color: white; | ||||
|  |         padding: 5px; | ||||
|  |         border-radius: 10px; | ||||
|  |         cursor: pointer; | ||||
|  |         box-shadow: 0 15px 70px rgba(0, 0, 0, 0.9); | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | .logo { | ||||
|  |     transition: all $easing-in 0.2s; | ||||
|  | 
 | ||||
|  |     &.edit-mode { | ||||
|  |         cursor: pointer; | ||||
|  | 
 | ||||
|  |         &:hover { | ||||
|  |             transform: scale(1.2); | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | .incident { | ||||
|  |     .content { | ||||
|  |         &[contenteditable=true] { | ||||
|  |             min-height: 60px; | ||||
|  |         } | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     .date { | ||||
|  |         font-size: 12px; | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | .mobile { | ||||
|  |     h1 { | ||||
|  |         font-size: 22px; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     .overall-status { | ||||
|  |         font-size: 20px; | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | </style> | ||||
| @ -1,70 +1,104 @@ | |||||
| "use strict"; | "use strict"; | ||||
| Object.defineProperty(exports, "__esModule", { value: true }); | // Common Util for frontend and backend
 | ||||
| exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0; | //
 | ||||
| const _dayjs = require("dayjs"); | // DOT NOT MODIFY util.js!
 | ||||
| const dayjs = _dayjs; | // Need to run "tsc" to compile if there are any changes.
 | ||||
| exports.isDev = process.env.NODE_ENV === "development"; | //
 | ||||
| exports.appName = "Uptime Kuma"; | // Backend uses the compiled file util.js
 | ||||
| exports.DOWN = 0; | // Frontend uses util.ts
 | ||||
| exports.UP = 1; | Object.defineProperty(exports, "__esModule", { value: true }); | ||||
| exports.PENDING = 2; | exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0; | ||||
| function flipStatus(s) { | const _dayjs = require("dayjs"); | ||||
|     if (s === exports.UP) { | const dayjs = _dayjs; | ||||
|         return exports.DOWN; | exports.isDev = process.env.NODE_ENV === "development"; | ||||
|     } | exports.appName = "Uptime Kuma"; | ||||
|     if (s === exports.DOWN) { | exports.DOWN = 0; | ||||
|         return exports.UP; | exports.UP = 1; | ||||
|     } | exports.PENDING = 2; | ||||
|     return s; | exports.STATUS_PAGE_ALL_DOWN = 0; | ||||
| } | exports.STATUS_PAGE_ALL_UP = 1; | ||||
| exports.flipStatus = flipStatus; | exports.STATUS_PAGE_PARTIAL_DOWN = 2; | ||||
| function sleep(ms) { | function flipStatus(s) { | ||||
|     return new Promise(resolve => setTimeout(resolve, ms)); |     if (s === exports.UP) { | ||||
| } |         return exports.DOWN; | ||||
| exports.sleep = sleep; |     } | ||||
| function ucfirst(str) { |     if (s === exports.DOWN) { | ||||
|     if (!str) { |         return exports.UP; | ||||
|         return str; |     } | ||||
|     } |     return s; | ||||
|     const firstLetter = str.substr(0, 1); | } | ||||
|     return firstLetter.toUpperCase() + str.substr(1); | exports.flipStatus = flipStatus; | ||||
| } | function sleep(ms) { | ||||
| exports.ucfirst = ucfirst; |     return new Promise(resolve => setTimeout(resolve, ms)); | ||||
| function debug(msg) { | } | ||||
|     if (exports.isDev) { | exports.sleep = sleep; | ||||
|         console.log(msg); | /** | ||||
|     } |  * PHP's ucfirst | ||||
| } |  * @param str | ||||
| exports.debug = debug; |  */ | ||||
| function polyfill() { | function ucfirst(str) { | ||||
|     if (!String.prototype.replaceAll) { |     if (!str) { | ||||
|         String.prototype.replaceAll = function (str, newStr) { |         return str; | ||||
|             if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") { |     } | ||||
|                 return this.replace(str, newStr); |     const firstLetter = str.substr(0, 1); | ||||
|             } |     return firstLetter.toUpperCase() + str.substr(1); | ||||
|             return this.replace(new RegExp(str, "g"), newStr); | } | ||||
|         }; | exports.ucfirst = ucfirst; | ||||
|     } | function debug(msg) { | ||||
| } |     if (exports.isDev) { | ||||
| exports.polyfill = polyfill; |         console.log(msg); | ||||
| class TimeLogger { |     } | ||||
|     constructor() { | } | ||||
|         this.startTime = dayjs().valueOf(); | exports.debug = debug; | ||||
|     } | function polyfill() { | ||||
|     print(name) { |     /** | ||||
|         if (exports.isDev) { |      * String.prototype.replaceAll() polyfill | ||||
|             console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms"); |      * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
 | ||||
|         } |      * @author Chris Ferdinandi | ||||
|     } |      * @license MIT | ||||
| } |      */ | ||||
| exports.TimeLogger = TimeLogger; |     if (!String.prototype.replaceAll) { | ||||
| function getRandomArbitrary(min, max) { |         String.prototype.replaceAll = function (str, newStr) { | ||||
|     return Math.random() * (max - min) + min; |             // If a regex pattern
 | ||||
| } |             if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") { | ||||
| exports.getRandomArbitrary = getRandomArbitrary; |                 return this.replace(str, newStr); | ||||
| function getRandomInt(min, max) { |             } | ||||
|     min = Math.ceil(min); |             // If a string
 | ||||
|     max = Math.floor(max); |             return this.replace(new RegExp(str, "g"), newStr); | ||||
|     return Math.floor(Math.random() * (max - min + 1)) + min; |         }; | ||||
| } |     } | ||||
| exports.getRandomInt = getRandomInt; | } | ||||
|  | exports.polyfill = polyfill; | ||||
|  | class TimeLogger { | ||||
|  |     constructor() { | ||||
|  |         this.startTime = dayjs().valueOf(); | ||||
|  |     } | ||||
|  |     print(name) { | ||||
|  |         if (exports.isDev) { | ||||
|  |             console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms"); | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
|  | exports.TimeLogger = TimeLogger; | ||||
|  | /** | ||||
|  |  * Returns a random number between min (inclusive) and max (exclusive) | ||||
|  |  */ | ||||
|  | function getRandomArbitrary(min, max) { | ||||
|  |     return Math.random() * (max - min) + min; | ||||
|  | } | ||||
|  | exports.getRandomArbitrary = getRandomArbitrary; | ||||
|  | /** | ||||
|  |  * From: https://stackoverflow.com/questions/1527803/generating-random-whole-numbers-in-javascript-in-a-specific-range
 | ||||
|  |  * | ||||
|  |  * Returns a random integer between min (inclusive) and max (inclusive). | ||||
|  |  * The value is no lower than min (or the next integer greater than min | ||||
|  |  * if min isn't an integer) and no greater than max (or the next integer | ||||
|  |  * lower than max if max isn't an integer). | ||||
|  |  * Using Math.round() will give you a non-uniform distribution! | ||||
|  |  */ | ||||
|  | function getRandomInt(min, max) { | ||||
|  |     min = Math.ceil(min); | ||||
|  |     max = Math.floor(max); | ||||
|  |     return Math.floor(Math.random() * (max - min + 1)) + min; | ||||
|  | } | ||||
|  | exports.getRandomInt = getRandomInt; | ||||
|  | |||||
| @ -0,0 +1,10 @@ | |||||
|  | FROM ubuntu | ||||
|  | WORKDIR /app | ||||
|  | RUN apt update && apt --yes install git curl | ||||
|  | RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - | ||||
|  | RUN apt --yes install nodejs | ||||
|  | RUN git clone https://github.com/louislam/uptime-kuma.git . | ||||
|  | RUN npm run setup | ||||
|  | 
 | ||||
|  | # Option 1. Try it | ||||
|  | RUN node server/server.js | ||||
					Loading…
					
					
				
		Reference in new issue