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> | |||
| @ -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