committed by
							
								
								GitHub
							
						
					
				
				 41 changed files with 3446 additions and 896 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; | 
				
			|||
								
									
										File diff suppressed because it is too large
									
								
							
						
					@ -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; | 
				
			|||
								
									
										File diff suppressed because it is too large
									
								
							
						
					@ -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,98 +1,104 @@ | 
				
			|||
"use strict"; | 
				
			|||
// Common Util for frontend and backend
 | 
				
			|||
// Backend uses the compiled file util.js
 | 
				
			|||
// Frontend uses util.ts
 | 
				
			|||
// Need to run "tsc" to compile if there are any changes.
 | 
				
			|||
Object.defineProperty(exports, "__esModule", { value: true }); | 
				
			|||
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"); | 
				
			|||
const dayjs = _dayjs; | 
				
			|||
exports.isDev = process.env.NODE_ENV === "development"; | 
				
			|||
exports.appName = "Uptime Kuma"; | 
				
			|||
exports.DOWN = 0; | 
				
			|||
exports.UP = 1; | 
				
			|||
exports.PENDING = 2; | 
				
			|||
function flipStatus(s) { | 
				
			|||
    if (s === exports.UP) { | 
				
			|||
        return exports.DOWN; | 
				
			|||
    } | 
				
			|||
    if (s === exports.DOWN) { | 
				
			|||
        return exports.UP; | 
				
			|||
    } | 
				
			|||
    return s; | 
				
			|||
} | 
				
			|||
exports.flipStatus = flipStatus; | 
				
			|||
function sleep(ms) { | 
				
			|||
    return new Promise(resolve => setTimeout(resolve, ms)); | 
				
			|||
} | 
				
			|||
exports.sleep = sleep; | 
				
			|||
/** | 
				
			|||
 * PHP's ucfirst | 
				
			|||
 * @param str | 
				
			|||
 */ | 
				
			|||
function ucfirst(str) { | 
				
			|||
    if (!str) { | 
				
			|||
        return str; | 
				
			|||
    } | 
				
			|||
    const firstLetter = str.substr(0, 1); | 
				
			|||
    return firstLetter.toUpperCase() + str.substr(1); | 
				
			|||
} | 
				
			|||
exports.ucfirst = ucfirst; | 
				
			|||
function debug(msg) { | 
				
			|||
    if (exports.isDev) { | 
				
			|||
        console.log(msg); | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
exports.debug = debug; | 
				
			|||
function polyfill() { | 
				
			|||
    /** | 
				
			|||
     * String.prototype.replaceAll() polyfill | 
				
			|||
     * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
 | 
				
			|||
     * @author Chris Ferdinandi | 
				
			|||
     * @license MIT | 
				
			|||
     */ | 
				
			|||
    if (!String.prototype.replaceAll) { | 
				
			|||
        String.prototype.replaceAll = function (str, newStr) { | 
				
			|||
            // If a regex pattern
 | 
				
			|||
            if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") { | 
				
			|||
                return this.replace(str, newStr); | 
				
			|||
            } | 
				
			|||
            // If a string
 | 
				
			|||
            return this.replace(new RegExp(str, "g"), newStr); | 
				
			|||
        }; | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
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; | 
				
			|||
"use strict"; | 
				
			|||
// Common Util for frontend and backend
 | 
				
			|||
//
 | 
				
			|||
// DOT NOT MODIFY util.js!
 | 
				
			|||
// Need to run "tsc" to compile if there are any changes.
 | 
				
			|||
//
 | 
				
			|||
// Backend uses the compiled file util.js
 | 
				
			|||
// Frontend uses util.ts
 | 
				
			|||
Object.defineProperty(exports, "__esModule", { value: true }); | 
				
			|||
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; | 
				
			|||
const _dayjs = require("dayjs"); | 
				
			|||
const dayjs = _dayjs; | 
				
			|||
exports.isDev = process.env.NODE_ENV === "development"; | 
				
			|||
exports.appName = "Uptime Kuma"; | 
				
			|||
exports.DOWN = 0; | 
				
			|||
exports.UP = 1; | 
				
			|||
exports.PENDING = 2; | 
				
			|||
exports.STATUS_PAGE_ALL_DOWN = 0; | 
				
			|||
exports.STATUS_PAGE_ALL_UP = 1; | 
				
			|||
exports.STATUS_PAGE_PARTIAL_DOWN = 2; | 
				
			|||
function flipStatus(s) { | 
				
			|||
    if (s === exports.UP) { | 
				
			|||
        return exports.DOWN; | 
				
			|||
    } | 
				
			|||
    if (s === exports.DOWN) { | 
				
			|||
        return exports.UP; | 
				
			|||
    } | 
				
			|||
    return s; | 
				
			|||
} | 
				
			|||
exports.flipStatus = flipStatus; | 
				
			|||
function sleep(ms) { | 
				
			|||
    return new Promise(resolve => setTimeout(resolve, ms)); | 
				
			|||
} | 
				
			|||
exports.sleep = sleep; | 
				
			|||
/** | 
				
			|||
 * PHP's ucfirst | 
				
			|||
 * @param str | 
				
			|||
 */ | 
				
			|||
function ucfirst(str) { | 
				
			|||
    if (!str) { | 
				
			|||
        return str; | 
				
			|||
    } | 
				
			|||
    const firstLetter = str.substr(0, 1); | 
				
			|||
    return firstLetter.toUpperCase() + str.substr(1); | 
				
			|||
} | 
				
			|||
exports.ucfirst = ucfirst; | 
				
			|||
function debug(msg) { | 
				
			|||
    if (exports.isDev) { | 
				
			|||
        console.log(msg); | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
exports.debug = debug; | 
				
			|||
function polyfill() { | 
				
			|||
    /** | 
				
			|||
     * String.prototype.replaceAll() polyfill | 
				
			|||
     * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
 | 
				
			|||
     * @author Chris Ferdinandi | 
				
			|||
     * @license MIT | 
				
			|||
     */ | 
				
			|||
    if (!String.prototype.replaceAll) { | 
				
			|||
        String.prototype.replaceAll = function (str, newStr) { | 
				
			|||
            // If a regex pattern
 | 
				
			|||
            if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") { | 
				
			|||
                return this.replace(str, newStr); | 
				
			|||
            } | 
				
			|||
            // If a string
 | 
				
			|||
            return this.replace(new RegExp(str, "g"), newStr); | 
				
			|||
        }; | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
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; | 
				
			|||
 | 
				
			|||
					Loading…
					
					
				
		Reference in new issue