92 changed files with 6619 additions and 2314 deletions
			
			
		
								
									Binary file not shown.
								
							
						
					@ -0,0 +1,7 @@ | 
				
			|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. | 
				
			|||
BEGIN TRANSACTION; | 
				
			|||
 | 
				
			|||
ALTER TABLE monitor | 
				
			|||
    ADD retry_interval INTEGER default 0 not null; | 
				
			|||
 | 
				
			|||
COMMIT; | 
				
			|||
@ -0,0 +1,30 @@ | 
				
			|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. | 
				
			|||
BEGIN TRANSACTION; | 
				
			|||
 | 
				
			|||
create table `group` | 
				
			|||
( | 
				
			|||
    id           INTEGER      not null | 
				
			|||
        constraint group_pk | 
				
			|||
            primary key autoincrement, | 
				
			|||
    name         VARCHAR(255) not null, | 
				
			|||
    created_date DATETIME              default (DATETIME('now')) not null, | 
				
			|||
    public       BOOLEAN               default 0 not null, | 
				
			|||
    active       BOOLEAN               default 1 not null, | 
				
			|||
    weight       BOOLEAN      NOT NULL DEFAULT 1000 | 
				
			|||
); | 
				
			|||
 | 
				
			|||
CREATE TABLE [monitor_group] | 
				
			|||
( | 
				
			|||
    [id]         INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, | 
				
			|||
    [monitor_id] INTEGER                           NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE, | 
				
			|||
    [group_id]   INTEGER                           NOT NULL REFERENCES [group] ([id]) ON DELETE CASCADE ON UPDATE CASCADE, | 
				
			|||
    weight BOOLEAN NOT NULL DEFAULT 1000 | 
				
			|||
); | 
				
			|||
 | 
				
			|||
CREATE INDEX [fk] | 
				
			|||
    ON [monitor_group] ( | 
				
			|||
                        [monitor_id], | 
				
			|||
                        [group_id]); | 
				
			|||
 | 
				
			|||
 | 
				
			|||
COMMIT; | 
				
			|||
@ -0,0 +1,18 @@ | 
				
			|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. | 
				
			|||
BEGIN TRANSACTION; | 
				
			|||
 | 
				
			|||
create table incident | 
				
			|||
( | 
				
			|||
    id INTEGER not null | 
				
			|||
        constraint incident_pk | 
				
			|||
            primary key autoincrement, | 
				
			|||
    title VARCHAR(255) not null, | 
				
			|||
    content TEXT not null, | 
				
			|||
    style VARCHAR(30) default 'warning' not null, | 
				
			|||
    created_date DATETIME default (DATETIME('now')) not null, | 
				
			|||
    last_updated_date DATETIME, | 
				
			|||
    pin BOOLEAN default 1 not null, | 
				
			|||
    active BOOLEAN default 1 not null | 
				
			|||
); | 
				
			|||
 | 
				
			|||
COMMIT; | 
				
			|||
@ -0,0 +1,19 @@ | 
				
			|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. | 
				
			|||
CREATE TABLE tag ( | 
				
			|||
	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | 
				
			|||
	name VARCHAR(255) NOT NULL, | 
				
			|||
    color VARCHAR(255) NOT NULL, | 
				
			|||
	created_date DATETIME DEFAULT (DATETIME('now')) NOT NULL | 
				
			|||
); | 
				
			|||
 | 
				
			|||
CREATE TABLE monitor_tag ( | 
				
			|||
	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | 
				
			|||
	monitor_id INTEGER NOT NULL, | 
				
			|||
	tag_id INTEGER NOT NULL, | 
				
			|||
	value TEXT, | 
				
			|||
	CONSTRAINT FK_tag FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE ON UPDATE CASCADE, | 
				
			|||
	CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor(id) ON DELETE CASCADE ON UPDATE CASCADE | 
				
			|||
); | 
				
			|||
 | 
				
			|||
CREATE INDEX monitor_tag_monitor_id_index ON monitor_tag (monitor_id); | 
				
			|||
CREATE INDEX monitor_tag_tag_id_index ON monitor_tag (tag_id); | 
				
			|||
@ -0,0 +1,21 @@ | 
				
			|||
#!/usr/bin/env sh | 
				
			|||
 | 
				
			|||
# set -e Exit the script if an error happens | 
				
			|||
set -e | 
				
			|||
PUID=${PUID=1000} | 
				
			|||
PGID=${PGID=1000} | 
				
			|||
 | 
				
			|||
files_ownership () { | 
				
			|||
    # -h Changes the ownership of an encountered symbolic link and not that of the file or directory pointed to by the symbolic link. | 
				
			|||
    # -R Recursively descends the specified directories | 
				
			|||
    # -c Like verbose but report only when a change is made | 
				
			|||
    chown -hRc "$PUID":"$PGID" /app/data | 
				
			|||
} | 
				
			|||
 | 
				
			|||
echo "==> Performing startup jobs and maintenance tasks" | 
				
			|||
files_ownership | 
				
			|||
 | 
				
			|||
echo "==> Starting application with user $PUID group $PGID" | 
				
			|||
 | 
				
			|||
# --clear-groups Clear supplementary groups. | 
				
			|||
exec setpriv --reuid "$PUID" --regid "$PGID" --clear-groups "$@" | 
				
			|||
								
									
										File diff suppressed because it is too large
									
								
							
						
					| 
		 After Width: | Height: | Size: 2.6 KiB  | 
| 
		 After Width: | Height: | Size: 9.5 KiB  | 
@ -0,0 +1,19 @@ | 
				
			|||
{ | 
				
			|||
    "name": "Uptime Kuma", | 
				
			|||
    "short_name": "Uptime Kuma", | 
				
			|||
    "start_url": "/", | 
				
			|||
    "background_color": "#fff", | 
				
			|||
    "display": "standalone", | 
				
			|||
    "icons": [ | 
				
			|||
        { | 
				
			|||
            "src": "icon-192x192.png", | 
				
			|||
            "sizes": "192x192", | 
				
			|||
            "type": "image/png" | 
				
			|||
        }, | 
				
			|||
        { | 
				
			|||
            "src": "icon-512x512.png", | 
				
			|||
            "sizes": "512x512", | 
				
			|||
            "type": "image/png" | 
				
			|||
        } | 
				
			|||
    ] | 
				
			|||
} | 
				
			|||
@ -0,0 +1,57 @@ | 
				
			|||
/* | 
				
			|||
    From https://github.com/DiegoZoracKy/image-data-uri/blob/master/lib/image-data-uri.js
 | 
				
			|||
    Modified with 0 dependencies | 
				
			|||
 */ | 
				
			|||
let fs = require("fs"); | 
				
			|||
 | 
				
			|||
let ImageDataURI = (() => { | 
				
			|||
 | 
				
			|||
    function decode(dataURI) { | 
				
			|||
        if (!/data:image\//.test(dataURI)) { | 
				
			|||
            console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\""); | 
				
			|||
            return null; | 
				
			|||
        } | 
				
			|||
 | 
				
			|||
        let regExMatches = dataURI.match("data:(image/.*);base64,(.*)"); | 
				
			|||
        return { | 
				
			|||
            imageType: regExMatches[1], | 
				
			|||
            dataBase64: regExMatches[2], | 
				
			|||
            dataBuffer: new Buffer(regExMatches[2], "base64") | 
				
			|||
        }; | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    function encode(data, mediaType) { | 
				
			|||
        if (!data || !mediaType) { | 
				
			|||
            console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType "); | 
				
			|||
            return null; | 
				
			|||
        } | 
				
			|||
 | 
				
			|||
        mediaType = (/\//.test(mediaType)) ? mediaType : "image/" + mediaType; | 
				
			|||
        let dataBase64 = (Buffer.isBuffer(data)) ? data.toString("base64") : new Buffer(data).toString("base64"); | 
				
			|||
        let dataImgBase64 = "data:" + mediaType + ";base64," + dataBase64; | 
				
			|||
 | 
				
			|||
        return dataImgBase64; | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    function outputFile(dataURI, filePath) { | 
				
			|||
        filePath = filePath || "./"; | 
				
			|||
        return new Promise((resolve, reject) => { | 
				
			|||
            let imageDecoded = decode(dataURI); | 
				
			|||
 | 
				
			|||
            fs.writeFile(filePath, imageDecoded.dataBuffer, err => { | 
				
			|||
                if (err) { | 
				
			|||
                    return reject("ImageDataURI :: Error :: " + JSON.stringify(err, null, 4)); | 
				
			|||
                } | 
				
			|||
                resolve(filePath); | 
				
			|||
            }); | 
				
			|||
        }); | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    return { | 
				
			|||
        decode: decode, | 
				
			|||
        encode: encode, | 
				
			|||
        outputFile: outputFile, | 
				
			|||
    }; | 
				
			|||
})(); | 
				
			|||
 | 
				
			|||
module.exports = ImageDataURI; | 
				
			|||
@ -0,0 +1,34 @@ | 
				
			|||
const { BeanModel } = require("redbean-node/dist/bean-model"); | 
				
			|||
const { R } = require("redbean-node"); | 
				
			|||
 | 
				
			|||
class Group extends BeanModel { | 
				
			|||
 | 
				
			|||
    async toPublicJSON() { | 
				
			|||
        let monitorBeanList = await this.getMonitorList(); | 
				
			|||
        let monitorList = []; | 
				
			|||
 | 
				
			|||
        for (let bean of monitorBeanList) { | 
				
			|||
            monitorList.push(await bean.toPublicJSON()); | 
				
			|||
        } | 
				
			|||
 | 
				
			|||
        return { | 
				
			|||
            id: this.id, | 
				
			|||
            name: this.name, | 
				
			|||
            weight: this.weight, | 
				
			|||
            monitorList, | 
				
			|||
        }; | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    async getMonitorList() { | 
				
			|||
        return R.convertToBeans("monitor", await R.getAll(` | 
				
			|||
            SELECT monitor.* FROM monitor, monitor_group | 
				
			|||
            WHERE monitor.id = monitor_group.monitor_id | 
				
			|||
            AND group_id = ? | 
				
			|||
            ORDER BY monitor_group.weight | 
				
			|||
        `, [
 | 
				
			|||
            this.id, | 
				
			|||
        ])); | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
 | 
				
			|||
module.exports = Group; | 
				
			|||
@ -0,0 +1,18 @@ | 
				
			|||
const { BeanModel } = require("redbean-node/dist/bean-model"); | 
				
			|||
 | 
				
			|||
class Incident extends BeanModel { | 
				
			|||
 | 
				
			|||
    toPublicJSON() { | 
				
			|||
        return { | 
				
			|||
            id: this.id, | 
				
			|||
            style: this.style, | 
				
			|||
            title: this.title, | 
				
			|||
            content: this.content, | 
				
			|||
            pin: this.pin, | 
				
			|||
            createdDate: this.createdDate, | 
				
			|||
            lastUpdatedDate: this.lastUpdatedDate, | 
				
			|||
        }; | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
 | 
				
			|||
module.exports = Incident; | 
				
			|||
@ -0,0 +1,13 @@ | 
				
			|||
const { BeanModel } = require("redbean-node/dist/bean-model"); | 
				
			|||
 | 
				
			|||
class Tag extends BeanModel { | 
				
			|||
    toJSON() { | 
				
			|||
        return { | 
				
			|||
            id: this._id, | 
				
			|||
            name: this._name, | 
				
			|||
            color: this._color, | 
				
			|||
        }; | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
 | 
				
			|||
module.exports = Tag; | 
				
			|||
@ -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,124 @@ | 
				
			|||
const NotificationProvider = require("./notification-provider"); | 
				
			|||
const axios = require("axios"); | 
				
			|||
const { DOWN, UP } = require("../../src/util"); | 
				
			|||
 | 
				
			|||
class Teams extends NotificationProvider { | 
				
			|||
    name = "teams"; | 
				
			|||
 | 
				
			|||
    _statusMessageFactory = (status, monitorName) => { | 
				
			|||
        if (status === DOWN) { | 
				
			|||
            return `🔴 Application [${monitorName}] went down`; | 
				
			|||
        } else if (status === UP) { | 
				
			|||
            return `✅ Application [${monitorName}] is back online`; | 
				
			|||
        } | 
				
			|||
        return "Notification"; | 
				
			|||
    }; | 
				
			|||
 | 
				
			|||
    _getThemeColor = (status) => { | 
				
			|||
        if (status === DOWN) { | 
				
			|||
            return "ff0000"; | 
				
			|||
        } | 
				
			|||
        if (status === UP) { | 
				
			|||
            return "00e804"; | 
				
			|||
        } | 
				
			|||
        return "008cff"; | 
				
			|||
    }; | 
				
			|||
 | 
				
			|||
    _notificationPayloadFactory = ({ | 
				
			|||
        status, | 
				
			|||
        monitorMessage, | 
				
			|||
        monitorName, | 
				
			|||
        monitorUrl, | 
				
			|||
    }) => { | 
				
			|||
        const notificationMessage = this._statusMessageFactory( | 
				
			|||
            status, | 
				
			|||
            monitorName | 
				
			|||
        ); | 
				
			|||
 | 
				
			|||
        const facts = []; | 
				
			|||
 | 
				
			|||
        if (monitorName) { | 
				
			|||
            facts.push({ | 
				
			|||
                name: "Monitor", | 
				
			|||
                value: monitorName, | 
				
			|||
            }); | 
				
			|||
        } | 
				
			|||
 | 
				
			|||
        if (monitorUrl) { | 
				
			|||
            facts.push({ | 
				
			|||
                name: "URL", | 
				
			|||
                value: monitorUrl, | 
				
			|||
            }); | 
				
			|||
        } | 
				
			|||
 | 
				
			|||
        return { | 
				
			|||
            "@context": "https://schema.org/extensions", | 
				
			|||
            "@type": "MessageCard", | 
				
			|||
            themeColor: this._getThemeColor(status), | 
				
			|||
            summary: notificationMessage, | 
				
			|||
            sections: [ | 
				
			|||
                { | 
				
			|||
                    activityImage: | 
				
			|||
                        "https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png", | 
				
			|||
                    activityTitle: "**Uptime Kuma**", | 
				
			|||
                }, | 
				
			|||
                { | 
				
			|||
                    activityTitle: notificationMessage, | 
				
			|||
                }, | 
				
			|||
                { | 
				
			|||
                    activityTitle: "**Description**", | 
				
			|||
                    text: monitorMessage, | 
				
			|||
                    facts, | 
				
			|||
                }, | 
				
			|||
            ], | 
				
			|||
        }; | 
				
			|||
    }; | 
				
			|||
 | 
				
			|||
    _sendNotification = async (webhookUrl, payload) => { | 
				
			|||
        await axios.post(webhookUrl, payload); | 
				
			|||
    }; | 
				
			|||
 | 
				
			|||
    _handleGeneralNotification = (webhookUrl, msg) => { | 
				
			|||
        const payload = this._notificationPayloadFactory({ | 
				
			|||
            monitorMessage: msg | 
				
			|||
        }); | 
				
			|||
 | 
				
			|||
        return this._sendNotification(webhookUrl, payload); | 
				
			|||
    }; | 
				
			|||
 | 
				
			|||
    async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { | 
				
			|||
        let okMsg = "Sent Successfully. "; | 
				
			|||
 | 
				
			|||
        try { | 
				
			|||
            if (heartbeatJSON == null) { | 
				
			|||
                await this._handleGeneralNotification(notification.webhookUrl, msg); | 
				
			|||
                return okMsg; | 
				
			|||
            } | 
				
			|||
 | 
				
			|||
            let url; | 
				
			|||
 | 
				
			|||
            if (monitorJSON["type"] === "port") { | 
				
			|||
                url = monitorJSON["hostname"]; | 
				
			|||
                if (monitorJSON["port"]) { | 
				
			|||
                    url += ":" + monitorJSON["port"]; | 
				
			|||
                } | 
				
			|||
            } else { | 
				
			|||
                url = monitorJSON["url"]; | 
				
			|||
            } | 
				
			|||
 | 
				
			|||
            const payload = this._notificationPayloadFactory({ | 
				
			|||
                monitorMessage: heartbeatJSON.msg, | 
				
			|||
                monitorName: monitorJSON.name, | 
				
			|||
                monitorUrl: url, | 
				
			|||
                status: heartbeatJSON.status, | 
				
			|||
            }); | 
				
			|||
 | 
				
			|||
            await this._sendNotification(notification.webhookUrl, payload); | 
				
			|||
            return okMsg; | 
				
			|||
        } catch (error) { | 
				
			|||
            this.throwGeneralAxiosError(error); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
 | 
				
			|||
module.exports = Teams; | 
				
			|||
@ -0,0 +1,151 @@ | 
				
			|||
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,73 @@ | 
				
			|||
<template> | 
				
			|||
    <div class="tag-wrapper rounded d-inline-flex" | 
				
			|||
         :class="{ 'px-3': size == 'normal', | 
				
			|||
                   'py-1': size == 'normal', | 
				
			|||
                   'm-2': size == 'normal', | 
				
			|||
                   'px-2': size == 'sm', | 
				
			|||
                   'py-0': size == 'sm', | 
				
			|||
                   'm-1': size == 'sm', | 
				
			|||
         }" | 
				
			|||
         :style="{ backgroundColor: item.color, fontSize: size == 'sm' ? '0.7em' : '1em' }" | 
				
			|||
    > | 
				
			|||
        <span class="tag-text">{{ displayText }}</span> | 
				
			|||
        <span v-if="remove != null" class="ps-1 btn-remove" @click="remove(item)"> | 
				
			|||
            <font-awesome-icon icon="times" /> | 
				
			|||
        </span> | 
				
			|||
    </div> | 
				
			|||
</template> | 
				
			|||
 | 
				
			|||
<script> | 
				
			|||
export default { | 
				
			|||
    props: { | 
				
			|||
        item: { | 
				
			|||
            type: Object, | 
				
			|||
            required: true, | 
				
			|||
        }, | 
				
			|||
        remove: { | 
				
			|||
            type: Function, | 
				
			|||
            default: null, | 
				
			|||
        }, | 
				
			|||
        size: { | 
				
			|||
            type: String, | 
				
			|||
            default: "normal", | 
				
			|||
        } | 
				
			|||
    }, | 
				
			|||
    computed: { | 
				
			|||
        displayText() { | 
				
			|||
            if (this.item.value == "") { | 
				
			|||
                return this.item.name; | 
				
			|||
            } else { | 
				
			|||
                return `${this.item.name}: ${this.item.value}`; | 
				
			|||
            } | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
</script> | 
				
			|||
 | 
				
			|||
<style lang="scss" scoped> | 
				
			|||
.tag-wrapper { | 
				
			|||
    color: white; | 
				
			|||
    opacity: 0.85; | 
				
			|||
 | 
				
			|||
    .dark & { | 
				
			|||
        opacity: 1; | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
 | 
				
			|||
.tag-text { | 
				
			|||
    padding-bottom: 1px !important; | 
				
			|||
    text-overflow: ellipsis; | 
				
			|||
    overflow: hidden; | 
				
			|||
    white-space: nowrap; | 
				
			|||
} | 
				
			|||
 | 
				
			|||
.btn-remove { | 
				
			|||
    font-size: 0.9em; | 
				
			|||
    line-height: 24px; | 
				
			|||
    opacity: 0.3; | 
				
			|||
} | 
				
			|||
 | 
				
			|||
.btn-remove:hover { | 
				
			|||
    opacity: 1; | 
				
			|||
} | 
				
			|||
</style> | 
				
			|||
@ -0,0 +1,405 @@ | 
				
			|||
<template> | 
				
			|||
    <div> | 
				
			|||
        <h4 class="mb-3">{{ $t("Tags") }}</h4> | 
				
			|||
        <div class="mb-3 p-1"> | 
				
			|||
            <tag | 
				
			|||
                v-for="item in selectedTags" | 
				
			|||
                :key="item.id" | 
				
			|||
                :item="item" | 
				
			|||
                :remove="deleteTag" | 
				
			|||
            /> | 
				
			|||
        </div> | 
				
			|||
        <div class="p-1"> | 
				
			|||
            <button | 
				
			|||
                type="button" | 
				
			|||
                class="btn btn-outline-secondary btn-add" | 
				
			|||
                :disabled="processing" | 
				
			|||
                @click.stop="showAddDialog" | 
				
			|||
            > | 
				
			|||
                <font-awesome-icon class="me-1" icon="plus" /> {{ $t("Add") }} | 
				
			|||
            </button> | 
				
			|||
        </div> | 
				
			|||
        <div ref="modal" class="modal fade" tabindex="-1"> | 
				
			|||
            <div class="modal-dialog modal-dialog-centered"> | 
				
			|||
                <div class="modal-content"> | 
				
			|||
                    <div class="modal-body"> | 
				
			|||
                        <vue-multiselect | 
				
			|||
                            v-model="newDraftTag.select" | 
				
			|||
                            class="mb-2" | 
				
			|||
                            :options="tagOptions" | 
				
			|||
                            :multiple="false" | 
				
			|||
                            :searchable="true" | 
				
			|||
                            :placeholder="$t('Add New below or Select...')" | 
				
			|||
                            track-by="id" | 
				
			|||
                            label="name" | 
				
			|||
                        > | 
				
			|||
                            <template #option="{ option }"> | 
				
			|||
                                <div class="mx-2 py-1 px-3 rounded d-inline-flex" | 
				
			|||
                                     style="margin-top: -5px; margin-bottom: -5px; height: 24px;" | 
				
			|||
                                     :style="{ color: textColor(option), backgroundColor: option.color + ' !important' }" | 
				
			|||
                                > | 
				
			|||
                                    <span> | 
				
			|||
                                        {{ option.name }}</span> | 
				
			|||
                                </div> | 
				
			|||
                            </template> | 
				
			|||
                            <template #singleLabel="{ option }"> | 
				
			|||
                                <div class="py-1 px-3 rounded d-inline-flex" | 
				
			|||
                                     style="height: 24px;" | 
				
			|||
                                     :style="{ color: textColor(option), backgroundColor: option.color + ' !important' }" | 
				
			|||
                                > | 
				
			|||
                                    <span>{{ option.name }}</span> | 
				
			|||
                                </div> | 
				
			|||
                            </template> | 
				
			|||
                        </vue-multiselect> | 
				
			|||
                        <div v-if="newDraftTag.select?.name == null" class="d-flex mb-2"> | 
				
			|||
                            <div class="w-50 pe-2"> | 
				
			|||
                                <input v-model="newDraftTag.name" class="form-control" | 
				
			|||
                                       :class="{'is-invalid': validateDraftTag.nameInvalid}" | 
				
			|||
                                       :placeholder="$t('Name')" | 
				
			|||
                                       @keydown.enter.prevent="onEnter" | 
				
			|||
                                /> | 
				
			|||
                                <div class="invalid-feedback"> | 
				
			|||
                                    {{ $t("Tag with this name already exist.") }} | 
				
			|||
                                </div> | 
				
			|||
                            </div> | 
				
			|||
                            <div class="w-50 ps-2"> | 
				
			|||
                                <vue-multiselect | 
				
			|||
                                    v-model="newDraftTag.color" | 
				
			|||
                                    :options="colorOptions" | 
				
			|||
                                    :multiple="false" | 
				
			|||
                                    :searchable="true" | 
				
			|||
                                    :placeholder="$t('color')" | 
				
			|||
                                    track-by="color" | 
				
			|||
                                    label="name" | 
				
			|||
                                    select-label="" | 
				
			|||
                                    deselect-label="" | 
				
			|||
                                > | 
				
			|||
                                    <template #option="{ option }"> | 
				
			|||
                                        <div class="mx-2 py-1 px-3 rounded d-inline-flex" | 
				
			|||
                                             style="height: 24px; color: white;" | 
				
			|||
                                             :style="{ backgroundColor: option.color + ' !important' }" | 
				
			|||
                                        > | 
				
			|||
                                            <span>{{ option.name }}</span> | 
				
			|||
                                        </div> | 
				
			|||
                                    </template> | 
				
			|||
                                    <template #singleLabel="{ option }"> | 
				
			|||
                                        <div class="py-1 px-3 rounded d-inline-flex" | 
				
			|||
                                             style="height: 24px; color: white;" | 
				
			|||
                                             :style="{ backgroundColor: option.color + ' !important' }" | 
				
			|||
                                        > | 
				
			|||
                                            <span>{{ option.name }}</span> | 
				
			|||
                                        </div> | 
				
			|||
                                    </template> | 
				
			|||
                                </vue-multiselect> | 
				
			|||
                            </div> | 
				
			|||
                        </div> | 
				
			|||
                        <div class="mb-2"> | 
				
			|||
                            <input v-model="newDraftTag.value" class="form-control" | 
				
			|||
                                   :class="{'is-invalid': validateDraftTag.valueInvalid}" | 
				
			|||
                                   :placeholder="$t('value (optional)')" | 
				
			|||
                                   @keydown.enter.prevent="onEnter" | 
				
			|||
                            /> | 
				
			|||
                            <div class="invalid-feedback"> | 
				
			|||
                                {{ $t("Tag with this value already exist.") }} | 
				
			|||
                            </div> | 
				
			|||
                        </div> | 
				
			|||
                        <div class="mb-2"> | 
				
			|||
                            <button | 
				
			|||
                                type="button" | 
				
			|||
                                class="btn btn-secondary float-end" | 
				
			|||
                                :disabled="processing || validateDraftTag.invalid" | 
				
			|||
                                @click.stop="addDraftTag" | 
				
			|||
                            > | 
				
			|||
                                {{ $t("Add") }} | 
				
			|||
                            </button> | 
				
			|||
                        </div> | 
				
			|||
                    </div> | 
				
			|||
                </div> | 
				
			|||
            </div> | 
				
			|||
        </div> | 
				
			|||
    </div> | 
				
			|||
</template> | 
				
			|||
 | 
				
			|||
<script> | 
				
			|||
import { Modal } from "bootstrap"; | 
				
			|||
import VueMultiselect from "vue-multiselect"; | 
				
			|||
import Tag from "../components/Tag.vue"; | 
				
			|||
import { useToast } from "vue-toastification" | 
				
			|||
const toast = useToast() | 
				
			|||
 | 
				
			|||
export default { | 
				
			|||
    components: { | 
				
			|||
        Tag, | 
				
			|||
        VueMultiselect, | 
				
			|||
    }, | 
				
			|||
    props: { | 
				
			|||
        preSelectedTags: { | 
				
			|||
            type: Array, | 
				
			|||
            default: () => [], | 
				
			|||
        }, | 
				
			|||
    }, | 
				
			|||
    data() { | 
				
			|||
        return { | 
				
			|||
            modal: null, | 
				
			|||
            existingTags: [], | 
				
			|||
            processing: false, | 
				
			|||
            newTags: [], | 
				
			|||
            deleteTags: [], | 
				
			|||
            newDraftTag: { | 
				
			|||
                name: null, | 
				
			|||
                select: null, | 
				
			|||
                color: null, | 
				
			|||
                value: "", | 
				
			|||
                invalid: true, | 
				
			|||
                nameInvalid: false, | 
				
			|||
            }, | 
				
			|||
        }; | 
				
			|||
    }, | 
				
			|||
    computed: { | 
				
			|||
        tagOptions() { | 
				
			|||
            const tagOptions = this.existingTags; | 
				
			|||
            for (const tag of this.newTags) { | 
				
			|||
                if (!tagOptions.find(t => t.name == tag.name && t.color == tag.color)) { | 
				
			|||
                    tagOptions.push(tag); | 
				
			|||
                } | 
				
			|||
            } | 
				
			|||
            return tagOptions; | 
				
			|||
        }, | 
				
			|||
        selectedTags() { | 
				
			|||
            return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id == tag.id)); | 
				
			|||
        }, | 
				
			|||
        colorOptions() { | 
				
			|||
            return [ | 
				
			|||
                { name: this.$t("Gray"), | 
				
			|||
                    color: "#4B5563" }, | 
				
			|||
                { name: this.$t("Red"), | 
				
			|||
                    color: "#DC2626" }, | 
				
			|||
                { name: this.$t("Orange"), | 
				
			|||
                    color: "#D97706" }, | 
				
			|||
                { name: this.$t("Green"), | 
				
			|||
                    color: "#059669" }, | 
				
			|||
                { name: this.$t("Blue"), | 
				
			|||
                    color: "#2563EB" }, | 
				
			|||
                { name: this.$t("Indigo"), | 
				
			|||
                    color: "#4F46E5" }, | 
				
			|||
                { name: this.$t("Purple"), | 
				
			|||
                    color: "#7C3AED" }, | 
				
			|||
                { name: this.$t("Pink"), | 
				
			|||
                    color: "#DB2777" }, | 
				
			|||
            ] | 
				
			|||
        }, | 
				
			|||
        validateDraftTag() { | 
				
			|||
            let nameInvalid = false; | 
				
			|||
            let valueInvalid = false; | 
				
			|||
            let invalid = true; | 
				
			|||
            if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value)) { | 
				
			|||
                // Undo removing a Tag | 
				
			|||
                nameInvalid = false; | 
				
			|||
                valueInvalid = false; | 
				
			|||
                invalid = false; | 
				
			|||
            } else if (this.existingTags.filter(tag => tag.name === this.newDraftTag.name).length > 0) { | 
				
			|||
                // Try to create new tag with existing name | 
				
			|||
                nameInvalid = true; | 
				
			|||
                invalid = true; | 
				
			|||
            } else if (this.newTags.concat(this.preSelectedTags).filter(tag => ( | 
				
			|||
                tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value | 
				
			|||
            ) || ( | 
				
			|||
                tag.name == this.newDraftTag.name && tag.value == this.newDraftTag.value | 
				
			|||
            )).length > 0) { | 
				
			|||
                // Try to add a tag with existing name and value | 
				
			|||
                valueInvalid = true; | 
				
			|||
                invalid = true; | 
				
			|||
            } else if (this.newDraftTag.select != null) { | 
				
			|||
                // Select an existing tag, no need to validate | 
				
			|||
                invalid = false; | 
				
			|||
                valueInvalid = false; | 
				
			|||
            } else if (this.newDraftTag.color == null || this.newDraftTag.name === "") { | 
				
			|||
                // Missing form inputs | 
				
			|||
                nameInvalid = false; | 
				
			|||
                invalid = true; | 
				
			|||
            } else { | 
				
			|||
                // Looks valid | 
				
			|||
                invalid = false; | 
				
			|||
                nameInvalid = false; | 
				
			|||
                valueInvalid = false; | 
				
			|||
            } | 
				
			|||
            return { | 
				
			|||
                invalid, | 
				
			|||
                nameInvalid, | 
				
			|||
                valueInvalid, | 
				
			|||
            } | 
				
			|||
        }, | 
				
			|||
    }, | 
				
			|||
    mounted() { | 
				
			|||
        this.modal = new Modal(this.$refs.modal); | 
				
			|||
        this.getExistingTags(); | 
				
			|||
    }, | 
				
			|||
    methods: { | 
				
			|||
        showAddDialog() { | 
				
			|||
            this.modal.show(); | 
				
			|||
        }, | 
				
			|||
        getExistingTags() { | 
				
			|||
            this.$root.getSocket().emit("getTags", (res) => { | 
				
			|||
                if (res.ok) { | 
				
			|||
                    this.existingTags = res.tags; | 
				
			|||
                } else { | 
				
			|||
                    toast.error(res.msg) | 
				
			|||
                } | 
				
			|||
            }); | 
				
			|||
        }, | 
				
			|||
        deleteTag(item) { | 
				
			|||
            if (item.new) { | 
				
			|||
                // Undo Adding a new Tag | 
				
			|||
                this.newTags = this.newTags.filter(tag => !(tag.name == item.name && tag.value == item.value)); | 
				
			|||
            } else { | 
				
			|||
                // Remove an Existing Tag | 
				
			|||
                this.deleteTags.push(item); | 
				
			|||
            } | 
				
			|||
        }, | 
				
			|||
        textColor(option) { | 
				
			|||
            if (option.color) { | 
				
			|||
                return "white"; | 
				
			|||
            } else { | 
				
			|||
                return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit"; | 
				
			|||
            } | 
				
			|||
        }, | 
				
			|||
        addDraftTag() { | 
				
			|||
            console.log("Adding Draft Tag: ", this.newDraftTag); | 
				
			|||
            if (this.newDraftTag.select != null) { | 
				
			|||
                if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value)) { | 
				
			|||
                    // Undo removing a tag | 
				
			|||
                    this.deleteTags = this.deleteTags.filter(tag => !(tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value)); | 
				
			|||
                } else { | 
				
			|||
                    // Add an existing Tag | 
				
			|||
                    this.newTags.push({ | 
				
			|||
                        id: this.newDraftTag.select.id, | 
				
			|||
                        color: this.newDraftTag.select.color, | 
				
			|||
                        name: this.newDraftTag.select.name, | 
				
			|||
                        value: this.newDraftTag.value, | 
				
			|||
                        new: true, | 
				
			|||
                    }) | 
				
			|||
                } | 
				
			|||
            } else { | 
				
			|||
                // Add new Tag | 
				
			|||
                this.newTags.push({ | 
				
			|||
                    color: this.newDraftTag.color.color, | 
				
			|||
                    name: this.newDraftTag.name.trim(), | 
				
			|||
                    value: this.newDraftTag.value, | 
				
			|||
                    new: true, | 
				
			|||
                }) | 
				
			|||
            } | 
				
			|||
            this.clearDraftTag(); | 
				
			|||
        }, | 
				
			|||
        clearDraftTag() { | 
				
			|||
            this.newDraftTag = { | 
				
			|||
                name: null, | 
				
			|||
                select: null, | 
				
			|||
                color: null, | 
				
			|||
                value: "", | 
				
			|||
                invalid: true, | 
				
			|||
                nameInvalid: false, | 
				
			|||
            }; | 
				
			|||
            this.modal.hide(); | 
				
			|||
        }, | 
				
			|||
        addTagAsync(newTag) { | 
				
			|||
            return new Promise((resolve) => { | 
				
			|||
                this.$root.getSocket().emit("addTag", newTag, resolve); | 
				
			|||
            }); | 
				
			|||
        }, | 
				
			|||
        addMonitorTagAsync(tagId, monitorId, value) { | 
				
			|||
            return new Promise((resolve) => { | 
				
			|||
                this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve); | 
				
			|||
            }); | 
				
			|||
        }, | 
				
			|||
        deleteMonitorTagAsync(tagId, monitorId, value) { | 
				
			|||
            return new Promise((resolve) => { | 
				
			|||
                this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve); | 
				
			|||
            }); | 
				
			|||
        }, | 
				
			|||
        onEnter() { | 
				
			|||
            if (!this.validateDraftTag.invalid) { | 
				
			|||
                this.addDraftTag(); | 
				
			|||
            } | 
				
			|||
        }, | 
				
			|||
        async submit(monitorId) { | 
				
			|||
            console.log(`Submitting tag changes for monitor ${monitorId}...`); | 
				
			|||
            this.processing = true; | 
				
			|||
 | 
				
			|||
            for (const newTag of this.newTags) { | 
				
			|||
                let tagId; | 
				
			|||
                if (newTag.id == null) { | 
				
			|||
                    // Create a New Tag | 
				
			|||
                    let newTagResult; | 
				
			|||
                    await this.addTagAsync(newTag).then((res) => { | 
				
			|||
                        if (!res.ok) { | 
				
			|||
                            toast.error(res.msg); | 
				
			|||
                            newTagResult = false; | 
				
			|||
                        } | 
				
			|||
                        newTagResult = res.tag; | 
				
			|||
                    }); | 
				
			|||
                    if (!newTagResult) { | 
				
			|||
                        // abort | 
				
			|||
                        this.processing = false; | 
				
			|||
                        return; | 
				
			|||
                    } | 
				
			|||
                    tagId = newTagResult.id; | 
				
			|||
                    // Assign the new ID to the tags of the same name & color | 
				
			|||
                    this.newTags.map(tag => { | 
				
			|||
                        if (tag.name == newTag.name && tag.color == newTag.color) { | 
				
			|||
                            tag.id = newTagResult.id; | 
				
			|||
                        } | 
				
			|||
                    }) | 
				
			|||
                } else { | 
				
			|||
                    tagId = newTag.id; | 
				
			|||
                } | 
				
			|||
 | 
				
			|||
                let newMonitorTagResult; | 
				
			|||
                // Assign tag to monitor | 
				
			|||
                await this.addMonitorTagAsync(tagId, monitorId, newTag.value).then((res) => { | 
				
			|||
                    if (!res.ok) { | 
				
			|||
                        toast.error(res.msg); | 
				
			|||
                        newMonitorTagResult = false; | 
				
			|||
                    } | 
				
			|||
                    newMonitorTagResult = true; | 
				
			|||
                }); | 
				
			|||
                if (!newMonitorTagResult) { | 
				
			|||
                    // abort | 
				
			|||
                    this.processing = false; | 
				
			|||
                    return; | 
				
			|||
                } | 
				
			|||
            } | 
				
			|||
 | 
				
			|||
            for (const deleteTag of this.deleteTags) { | 
				
			|||
                let deleteMonitorTagResult; | 
				
			|||
                await this.deleteMonitorTagAsync(deleteTag.tag_id, deleteTag.monitor_id, deleteTag.value).then((res) => { | 
				
			|||
                    if (!res.ok) { | 
				
			|||
                        toast.error(res.msg); | 
				
			|||
                        deleteMonitorTagResult = false; | 
				
			|||
                    } | 
				
			|||
                    deleteMonitorTagResult = true; | 
				
			|||
                }); | 
				
			|||
                if (!deleteMonitorTagResult) { | 
				
			|||
                    // abort | 
				
			|||
                    this.processing = false; | 
				
			|||
                    return; | 
				
			|||
                } | 
				
			|||
            } | 
				
			|||
 | 
				
			|||
            this.getExistingTags(); | 
				
			|||
            this.newTags = []; | 
				
			|||
            this.deleteTags = []; | 
				
			|||
            this.processing = false; | 
				
			|||
        } | 
				
			|||
    }, | 
				
			|||
}; | 
				
			|||
</script> | 
				
			|||
 | 
				
			|||
<style scoped> | 
				
			|||
.btn-add { | 
				
			|||
    width: 100%; | 
				
			|||
} | 
				
			|||
 | 
				
			|||
.modal-body { | 
				
			|||
    padding: 1.5rem; | 
				
			|||
} | 
				
			|||
</style> | 
				
			|||
@ -0,0 +1,29 @@ | 
				
			|||
<template> | 
				
			|||
    <div class="mb-3"> | 
				
			|||
        <label for="teams-webhookurl" class="form-label">Webhook URL</label> | 
				
			|||
        <input | 
				
			|||
            id="teams-webhookurl" | 
				
			|||
            v-model="$parent.notification.webhookUrl" | 
				
			|||
            type="text" | 
				
			|||
            class="form-control" | 
				
			|||
            required | 
				
			|||
        /> | 
				
			|||
        <div class="form-text"> | 
				
			|||
            You can learn how to create a webhook url | 
				
			|||
            <a | 
				
			|||
                href="https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook" | 
				
			|||
                target="_blank" | 
				
			|||
            >here</a>. | 
				
			|||
        </div> | 
				
			|||
    </div> | 
				
			|||
</template> | 
				
			|||
 | 
				
			|||
<script> | 
				
			|||
export default { | 
				
			|||
    data() { | 
				
			|||
        return { | 
				
			|||
            name: "teams", | 
				
			|||
        }; | 
				
			|||
    }, | 
				
			|||
}; | 
				
			|||
</script> | 
				
			|||
@ -0,0 +1,50 @@ | 
				
			|||
import { createI18n } from "vue-i18n"; | 
				
			|||
import daDK from "./languages/da-DK"; | 
				
			|||
import deDE from "./languages/de-DE"; | 
				
			|||
import en from "./languages/en"; | 
				
			|||
import esEs from "./languages/es-ES"; | 
				
			|||
import ptBR from "./languages/pt-BR"; | 
				
			|||
import etEE from "./languages/et-EE"; | 
				
			|||
import frFR from "./languages/fr-FR"; | 
				
			|||
import itIT from "./languages/it-IT"; | 
				
			|||
import ja from "./languages/ja"; | 
				
			|||
import koKR from "./languages/ko-KR"; | 
				
			|||
import nlNL from "./languages/nl-NL"; | 
				
			|||
import pl from "./languages/pl"; | 
				
			|||
import ruRU from "./languages/ru-RU"; | 
				
			|||
import sr from "./languages/sr"; | 
				
			|||
import srLatn from "./languages/sr-latn"; | 
				
			|||
import trTR from "./languages/tr-TR"; | 
				
			|||
import svSE from "./languages/sv-SE"; | 
				
			|||
import zhCN from "./languages/zh-CN"; | 
				
			|||
import zhHK from "./languages/zh-HK"; | 
				
			|||
 | 
				
			|||
const languageList = { | 
				
			|||
    en, | 
				
			|||
    "zh-HK": zhHK, | 
				
			|||
    "de-DE": deDE, | 
				
			|||
    "nl-NL": nlNL, | 
				
			|||
    "es-ES": esEs, | 
				
			|||
    "pt-BR": ptBR, | 
				
			|||
    "fr-FR": frFR, | 
				
			|||
    "it-IT": itIT, | 
				
			|||
    "ja": ja, | 
				
			|||
    "da-DK": daDK, | 
				
			|||
    "sr": sr, | 
				
			|||
    "sr-latn": srLatn, | 
				
			|||
    "sv-SE": svSE, | 
				
			|||
    "tr-TR": trTR, | 
				
			|||
    "ko-KR": koKR, | 
				
			|||
    "ru-RU": ruRU, | 
				
			|||
    "zh-CN": zhCN, | 
				
			|||
    "pl": pl, | 
				
			|||
    "et-EE": etEE, | 
				
			|||
}; | 
				
			|||
 | 
				
			|||
export const i18n = createI18n({ | 
				
			|||
    locale: localStorage.locale || "en", | 
				
			|||
    fallbackLocale: "en", | 
				
			|||
    silentFallbackWarn: true, | 
				
			|||
    silentTranslationWarn: true, | 
				
			|||
    messages: languageList, | 
				
			|||
}); | 
				
			|||
@ -0,0 +1,182 @@ | 
				
			|||
export default { | 
				
			|||
    languageName: "Português (Brasileiro)", | 
				
			|||
    checkEverySecond: "Verificar cada {0} segundos.", | 
				
			|||
    retryCheckEverySecond: "Tentar novamente a cada {0} segundos.", | 
				
			|||
    retriesDescription: "Máximo de tentativas antes que o serviço seja marcado como inativo e uma notificação seja enviada", | 
				
			|||
    ignoreTLSError: "Ignorar erros TLS/SSL para sites HTTPS", | 
				
			|||
    upsideDownModeDescription: "Inverta o status de cabeça para baixo. Se o serviço estiver acessível, ele está OFFLINE.", | 
				
			|||
    maxRedirectDescription: "Número máximo de redirecionamentos a seguir. Defina como 0 para desativar redirecionamentos.", | 
				
			|||
    acceptedStatusCodesDescription: "Selecione os códigos de status que são considerados uma resposta bem-sucedida.", | 
				
			|||
    passwordNotMatchMsg: "A senha repetida não corresponde.", | 
				
			|||
    notificationDescription: "Atribua uma notificação ao (s) monitor (es) para que funcione.", | 
				
			|||
    keywordDescription: "Pesquise a palavra-chave em html simples ou resposta JSON e diferencia maiúsculas de minúsculas", | 
				
			|||
    pauseDashboardHome: "Pausar", | 
				
			|||
    deleteMonitorMsg: "Tem certeza de que deseja excluir este monitor?", | 
				
			|||
    deleteNotificationMsg: "Tem certeza de que deseja excluir esta notificação para todos os monitores?", | 
				
			|||
    resoverserverDescription: "Cloudflare é o servidor padrão, você pode alterar o servidor resolvedor a qualquer momento.", | 
				
			|||
    rrtypeDescription: "Selecione o RR-Type que você deseja monitorar", | 
				
			|||
    pauseMonitorMsg: "Tem certeza que deseja fazer uma pausa?", | 
				
			|||
    enableDefaultNotificationDescription: "Para cada novo monitor, esta notificação será habilitada por padrão. Você ainda pode desativar a notificação separadamente para cada monitor.", | 
				
			|||
    clearEventsMsg: "Tem certeza de que deseja excluir todos os eventos deste monitor?", | 
				
			|||
    clearHeartbeatsMsg: "Tem certeza de que deseja excluir todos os heartbeats deste monitor?", | 
				
			|||
    confirmClearStatisticsMsg: "Tem certeza que deseja excluir TODAS as estatísticas?", | 
				
			|||
    importHandleDescription: "Escolha 'Ignorar existente' se quiser ignorar todos os monitores ou notificações com o mesmo nome. 'Substituir' excluirá todos os monitores e notificações existentes.", | 
				
			|||
    confirmImportMsg: "Tem certeza que deseja importar o backup? Certifique-se de que selecionou a opção de importação correta.", | 
				
			|||
    twoFAVerifyLabel: "Digite seu token para verificar se 2FA está funcionando", | 
				
			|||
    tokenValidSettingsMsg: "O token é válido! Agora você pode salvar as configurações 2FA.", | 
				
			|||
    confirmEnableTwoFAMsg: "Tem certeza de que deseja habilitar 2FA?", | 
				
			|||
    confirmDisableTwoFAMsg: "Tem certeza de que deseja desativar 2FA?", | 
				
			|||
    Settings: "Configurações", | 
				
			|||
    Dashboard: "Dashboard", | 
				
			|||
    "New Update": "Nova Atualização", | 
				
			|||
    Language: "Linguagem", | 
				
			|||
    Appearance: "Aparência", | 
				
			|||
    Theme: "Tema", | 
				
			|||
    General: "Geral", | 
				
			|||
    Version: "Versão", | 
				
			|||
    "Check Update On GitHub": "Verificar atualização no Github", | 
				
			|||
    List: "Lista", | 
				
			|||
    Add: "Adicionar", | 
				
			|||
    "Add New Monitor": "Adicionar novo monitor", | 
				
			|||
    "Quick Stats": "Estatísticas rápidas", | 
				
			|||
    Up: "On", | 
				
			|||
    Down: "Off", | 
				
			|||
    Pending: "Pendente", | 
				
			|||
    Unknown: "Desconhecido", | 
				
			|||
    Pause: "Pausar", | 
				
			|||
    Name: "Nome", | 
				
			|||
    Status: "Status", | 
				
			|||
    DateTime: "Data hora", | 
				
			|||
    Message: "Mensagem", | 
				
			|||
    "No important events": "Nenhum evento importante", | 
				
			|||
    Resume: "Resumo", | 
				
			|||
    Edit: "Editar", | 
				
			|||
    Delete: "Deletar", | 
				
			|||
    Current: "Atual", | 
				
			|||
    Uptime: "Tempo de atividade", | 
				
			|||
    "Cert Exp.": "Cert Exp.", | 
				
			|||
    days: "dias", | 
				
			|||
    day: "dia", | 
				
			|||
    "-day": "-dia", | 
				
			|||
    hour: "hora", | 
				
			|||
    "-hour": "-hora", | 
				
			|||
    Response: "Resposta", | 
				
			|||
    Ping: "Ping", | 
				
			|||
    "Monitor Type": "Tipo de Monitor", | 
				
			|||
    Keyword: "Palavra-Chave", | 
				
			|||
    "Friendly Name": "Nome Amigável", | 
				
			|||
    URL: "URL", | 
				
			|||
    Hostname: "Hostname", | 
				
			|||
    Port: "Porta", | 
				
			|||
    "Heartbeat Interval": "Intervalo de Heartbeat", | 
				
			|||
    Retries: "Novas tentativas", | 
				
			|||
    "Heartbeat Retry Interval": "Intervalo de repetição de Heartbeat", | 
				
			|||
    Advanced: "Avançado", | 
				
			|||
    "Upside Down Mode": "Modo de cabeça para baixo", | 
				
			|||
    "Max. Redirects": "Redirecionamento Máx.", | 
				
			|||
    "Accepted Status Codes": "Status Code Aceitáveis", | 
				
			|||
    Save: "Salvar", | 
				
			|||
    Notifications: "Notificações", | 
				
			|||
    "Not available, please setup.": "Não disponível, por favor configure.", | 
				
			|||
    "Setup Notification": "Configurar Notificação", | 
				
			|||
    Light: "Claro", | 
				
			|||
    Dark: "Escuro", | 
				
			|||
    Auto: "Auto", | 
				
			|||
    "Theme - Heartbeat Bar": "Tema - Barra de Heartbeat", | 
				
			|||
    Normal: "Normal", | 
				
			|||
    Bottom: "Inferior", | 
				
			|||
    None: "Nenhum", | 
				
			|||
    Timezone: "Fuso horário", | 
				
			|||
    "Search Engine Visibility": "Visibilidade do mecanismo de pesquisa", | 
				
			|||
    "Allow indexing": "Permitir Indexação", | 
				
			|||
    "Discourage search engines from indexing site": "Desencoraje os motores de busca de indexar o site", | 
				
			|||
    "Change Password": "Mudar senha", | 
				
			|||
    "Current Password": "Senha atual", | 
				
			|||
    "New Password": "Nova Senha", | 
				
			|||
    "Repeat New Password": "Repetir Nova Senha", | 
				
			|||
    "Update Password": "Atualizar Senha", | 
				
			|||
    "Disable Auth": "Desativar Autenticação", | 
				
			|||
    "Enable Auth": "Ativar Autenticação", | 
				
			|||
    Logout: "Deslogar", | 
				
			|||
    Leave: "Sair", | 
				
			|||
    "I understand, please disable": "Eu entendo, por favor desative.", | 
				
			|||
    Confirm: "Confirmar", | 
				
			|||
    Yes: "Sim", | 
				
			|||
    No: "Não", | 
				
			|||
    Username: "Usuário", | 
				
			|||
    Password: "Senha", | 
				
			|||
    "Remember me": "Lembre-me", | 
				
			|||
    Login: "Autenticar", | 
				
			|||
    "No Monitors, please": "Nenhum monitor, por favor", | 
				
			|||
    "add one": "adicionar um", | 
				
			|||
    "Notification Type": "Tipo de Notificação", | 
				
			|||
    Email: "Email", | 
				
			|||
    Test: "Testar", | 
				
			|||
    "Certificate Info": "Info. do Certificado ", | 
				
			|||
    "Resolver Server": "Resolver Servidor", | 
				
			|||
    "Resource Record Type": "Tipo de registro de aplicação", | 
				
			|||
    "Last Result": "Último resultado", | 
				
			|||
    "Create your admin account": "Crie sua conta de admin", | 
				
			|||
    "Repeat Password": "Repita a senha", | 
				
			|||
    "Import Backup": "Importar Backup", | 
				
			|||
    "Export Backup": "Exportar Backup", | 
				
			|||
    Export: "Exportar", | 
				
			|||
    Import: "Importar", | 
				
			|||
    respTime: "Tempo de Resp. (ms)", | 
				
			|||
    notAvailableShort: "N/A", | 
				
			|||
    "Default enabled": "Padrão habilitado", | 
				
			|||
    "Apply on all existing monitors": "Aplicar em todos os monitores existentes", | 
				
			|||
    Create: "Criar", | 
				
			|||
    "Clear Data": "Limpar Dados", | 
				
			|||
    Events: "Eventos", | 
				
			|||
    Heartbeats: "Heartbeats", | 
				
			|||
    "Auto Get": "Obter Automático", | 
				
			|||
    backupDescription: "Você pode fazer backup de todos os monitores e todas as notificações em um arquivo JSON.", | 
				
			|||
    backupDescription2: "OBS: Os dados do histórico e do evento não estão incluídos.", | 
				
			|||
    backupDescription3: "Dados confidenciais, como tokens de notificação, estão incluídos no arquivo de exportação, mantenha-o com cuidado.", | 
				
			|||
    alertNoFile: "Selecione um arquivo para importar.", | 
				
			|||
    alertWrongFileType: "Selecione um arquivo JSON.", | 
				
			|||
    "Clear all statistics": "Limpar todas as estatísticas", | 
				
			|||
    "Skip existing": "Pular existente", | 
				
			|||
    Overwrite: "Sobrescrever", | 
				
			|||
    Options: "Opções", | 
				
			|||
    "Keep both": "Manter os dois", | 
				
			|||
    "Verify Token": "Verificar Token", | 
				
			|||
    "Setup 2FA": "Configurar 2FA", | 
				
			|||
    "Enable 2FA": "Ativar 2FA", | 
				
			|||
    "Disable 2FA": "Desativar 2FA", | 
				
			|||
    "2FA Settings": "Configurações do 2FA ", | 
				
			|||
    "Two Factor Authentication": "Autenticação e Dois Fatores", | 
				
			|||
    Active: "Ativo", | 
				
			|||
    Inactive: "Inativo", | 
				
			|||
    Token: "Token", | 
				
			|||
    "Show URI": "Mostrar URI", | 
				
			|||
    Tags: "Tag", | 
				
			|||
    "Add New below or Select...": "Adicionar Novo abaixo ou Selecionar ...", | 
				
			|||
    "Tag with this name already exist.": "Já existe uma etiqueta com este nome.", | 
				
			|||
    "Tag with this value already exist.": "Já existe uma etiqueta com este valor.", | 
				
			|||
    color: "cor", | 
				
			|||
    "value (optional)": "valor (opcional)", | 
				
			|||
    Gray: "Cinza", | 
				
			|||
    Red: "Vermelho", | 
				
			|||
    Orange: "Laranja", | 
				
			|||
    Green: "Verde", | 
				
			|||
    Blue: "Azul", | 
				
			|||
    Indigo: "Índigo", | 
				
			|||
    Purple: "Roxo", | 
				
			|||
    Pink: "Rosa", | 
				
			|||
    "Search...": "Buscar...", | 
				
			|||
    "Avg. Ping": "Ping Médio.", | 
				
			|||
    "Avg. Response": "Resposta Média. ", | 
				
			|||
    "Status Page": "Página de Status", | 
				
			|||
    "Entry Page": "Página de entrada", | 
				
			|||
    "statusPageNothing": "Nada aqui, por favor, adicione um grupo ou monitor.", | 
				
			|||
    "No Services": "Nenhum Serviço", | 
				
			|||
    "All Systems Operational": "Todos os Serviços Operacionais", | 
				
			|||
    "Partially Degraded Service": "Serviço parcialmente degradado", | 
				
			|||
    "Degraded Service": "Serviço Degradado", | 
				
			|||
    "Add Group": "Adicionar Grupo", | 
				
			|||
    "Add a monitor": "Adicionar um monitor", | 
				
			|||
    "Edit Status Page": "Editar Página de Status", | 
				
			|||
    "Go to Dashboard": "Ir para a dashboard", | 
				
			|||
}; | 
				
			|||
@ -0,0 +1,181 @@ | 
				
			|||
export default { | 
				
			|||
    languageName: "Türkçe", | 
				
			|||
    checkEverySecond: "{0} Saniyede bir kontrol et.", | 
				
			|||
    retriesDescription: "Servisin kapalı olarak işaretlenmeden ve bir bildirim gönderilmeden önce maksimum yeniden deneme sayısı", | 
				
			|||
    ignoreTLSError: "HTTPS web siteleri için TLS/SSL hatasını yoksay", | 
				
			|||
    upsideDownModeDescription: "Servisin durumunu tersine çevirir. Servis çalışıyorsa kapalı olarak işaretler.", | 
				
			|||
    maxRedirectDescription: "İzlenecek maksimum yönlendirme sayısı. Yönlendirmeleri devre dışı bırakmak için 0'a ayarlayın.", | 
				
			|||
    acceptedStatusCodesDescription: "Servisin çalıştığını hangi durum kodları belirlesin?", | 
				
			|||
    passwordNotMatchMsg: "Şifre eşleşmiyor.", | 
				
			|||
    notificationDescription: "Servislerin bildirim gönderebilmesi için bir bildirim yöntemi belirleyin.", | 
				
			|||
    keywordDescription: "Anahtar kelimeyi düz html veya JSON yanıtında arayın ve büyük/küçük harfe duyarlıdır", | 
				
			|||
    pauseDashboardHome: "Durdur", | 
				
			|||
    deleteMonitorMsg: "Servisi silmek istediğinden emin misin?", | 
				
			|||
    deleteNotificationMsg: "Bu bildirimi tüm servisler için silmek istediğinden emin misin?", | 
				
			|||
    resoverserverDescription: "Cloudflare varsayılan sunucudur, çözümleyici sunucusunu istediğiniz zaman değiştirebilirsiniz.", | 
				
			|||
    rrtypeDescription: "İzlemek istediğiniz servisin RR-Tipini seçin", | 
				
			|||
    pauseMonitorMsg: "Durdurmak istediğinden emin misin?", | 
				
			|||
    clearEventsMsg: "Bu servisin bütün kayıtlarını silmek istediğinden emin misin?", | 
				
			|||
    clearHeartbeatsMsg: "Bu servis için tüm sağlık durumunu silmek istediğinden emin misin?", | 
				
			|||
    confirmClearStatisticsMsg: "Tüm istatistikleri silmek istediğinden emin misin?", | 
				
			|||
    Settings: "Ayarlar", | 
				
			|||
    Dashboard: "Panel", | 
				
			|||
    "New Update": "Yeni Güncelleme", | 
				
			|||
    Language: "Dil", | 
				
			|||
    Appearance: "Görünüm", | 
				
			|||
    Theme: "Tema", | 
				
			|||
    General: "Genel", | 
				
			|||
    Version: "Versiyon", | 
				
			|||
    "Check Update On GitHub": "GitHub'da Güncellemeyi Kontrol Edin", | 
				
			|||
    List: "Liste", | 
				
			|||
    Add: "Ekle", | 
				
			|||
    "Add New Monitor": "Yeni Servis Ekle", | 
				
			|||
    "Quick Stats": "Servis istatistikleri", | 
				
			|||
    Up: "Normal", | 
				
			|||
    Down: "Hatalı", | 
				
			|||
    Pending: "Bekliyor", | 
				
			|||
    Unknown: "Bilinmeyen", | 
				
			|||
    Pause: "Durdur", | 
				
			|||
    Name: "Servis ismi", | 
				
			|||
    Status: "Durum", | 
				
			|||
    DateTime: "Zaman", | 
				
			|||
    Message: "Mesaj", | 
				
			|||
    "No important events": "Önemli olay yok", | 
				
			|||
    Resume: "Devam et", | 
				
			|||
    Edit: "Düzenle", | 
				
			|||
    Delete: "Sil", | 
				
			|||
    Current: "Şu anda", | 
				
			|||
    Uptime: "Çalışma zamanı", | 
				
			|||
    "Cert Exp.": "Sertifika Süresi", | 
				
			|||
    days: "günler", | 
				
			|||
    day: "gün", | 
				
			|||
    "-day": "-gün", | 
				
			|||
    hour: "saat", | 
				
			|||
    "-hour": "-saat", | 
				
			|||
    Response: "Cevap Süresi", | 
				
			|||
    Ping: "Ping", | 
				
			|||
    "Monitor Type": "Servis Tipi", | 
				
			|||
    Keyword: "Anahtar Kelime", | 
				
			|||
    "Friendly Name": "Panelde görünecek isim", | 
				
			|||
    URL: "URL", | 
				
			|||
    Hostname: "IP Adresi", | 
				
			|||
    Port: "Port", | 
				
			|||
    "Heartbeat Interval": "Servis Test Aralığı", | 
				
			|||
    Retries: "Yeniden deneme", | 
				
			|||
    Advanced: "Gelişmiş", | 
				
			|||
    "Upside Down Mode": "Ters/Düz Modu", | 
				
			|||
    "Max. Redirects": "Maksimum Yönlendirme", | 
				
			|||
    "Accepted Status Codes": "Kabul Edilen Durum Kodları", | 
				
			|||
    Save: "Kaydet", | 
				
			|||
    Notifications: "Bildirimler", | 
				
			|||
    "Not available, please setup.": "Atanmış bildirim yöntemi yok. Ayarlardan belirleyebilirsiniz.", | 
				
			|||
    "Setup Notification": "Bildirim yöntemi kur", | 
				
			|||
    Light: "Açık", | 
				
			|||
    Dark: "Koyu", | 
				
			|||
    Auto: "Oto", | 
				
			|||
    "Theme - Heartbeat Bar": "Servis Bar Konumu", | 
				
			|||
    Normal: "Normal", | 
				
			|||
    Bottom: "Aşağıda", | 
				
			|||
    None: "Gösterme", | 
				
			|||
    Timezone: "Zaman Dilimi", | 
				
			|||
    "Search Engine Visibility": "Arama Motoru Görünürlüğü", | 
				
			|||
    "Allow indexing": "İndekslemeye izin ver", | 
				
			|||
    "Discourage search engines from indexing site": "İndekslemeyi reddet", | 
				
			|||
    "Change Password": "Şifre Değiştir", | 
				
			|||
    "Current Password": "Şuan ki Şifre", | 
				
			|||
    "New Password": "Yeni Şifre", | 
				
			|||
    "Repeat New Password": "Yeni Şifreyi Tekrar Girin", | 
				
			|||
    "Update Password": "Şifreyi Değiştir", | 
				
			|||
    "Disable Auth": "Şifreli girişi iptal et.", | 
				
			|||
    "Enable Auth": "Şifreli girişi aktif et.", | 
				
			|||
    Logout: "Çıkış yap", | 
				
			|||
    Leave: "Ayrıl", | 
				
			|||
    "I understand, please disable": "Evet farkındayım, iptal et", | 
				
			|||
    Confirm: "Onayla", | 
				
			|||
    Yes: "Evet", | 
				
			|||
    No: "Hayır", | 
				
			|||
    Username: "Kullanıcı Adı", | 
				
			|||
    Password: "Şifre", | 
				
			|||
    "Remember me": "Beni Hatırla", | 
				
			|||
    Login: "Giriş yap", | 
				
			|||
    "No Monitors, please": "Servis yok, lütfen", | 
				
			|||
    "add one": "bir servis ekleyin", | 
				
			|||
    "Notification Type": "Bildirim Yöntemi", | 
				
			|||
    Email: "E-mail", | 
				
			|||
    Test: "Test", | 
				
			|||
    "Certificate Info": "Sertifika Bilgisi", | 
				
			|||
    "Resolver Server": "Çözümleyici Sunucu", | 
				
			|||
    "Resource Record Type": "Kaynak Kayıt Türü", | 
				
			|||
    "Last Result": "En son sonuçlar", | 
				
			|||
    "Create your admin account": "Yönetici hesabınızı oluşturun", | 
				
			|||
    "Repeat Password": "Şifrenizi tekrar girin", | 
				
			|||
    respTime: "Cevap Süresi (ms)", | 
				
			|||
    notAvailableShort: "N/A", | 
				
			|||
    Create: "Yarat", | 
				
			|||
    "Clear Data": "Verileri Temizle", | 
				
			|||
    Events: "Olaylar", | 
				
			|||
    Heartbeats: "Sağlık Durumları", | 
				
			|||
    "Auto Get": "Otomatik Al", | 
				
			|||
    retryCheckEverySecond: "Retry every {0} seconds.", | 
				
			|||
    enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", | 
				
			|||
    importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.", | 
				
			|||
    confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.", | 
				
			|||
    twoFAVerifyLabel: "Please type in your token to verify that 2FA is working", | 
				
			|||
    tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.", | 
				
			|||
    confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?", | 
				
			|||
    confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?", | 
				
			|||
    "Heartbeat Retry Interval": "Heartbeat Retry Interval", | 
				
			|||
    "Import Backup": "Import Backup", | 
				
			|||
    "Export Backup": "Export Backup", | 
				
			|||
    Export: "Export", | 
				
			|||
    Import: "Import", | 
				
			|||
    "Default enabled": "Default enabled", | 
				
			|||
    "Apply on all existing monitors": "Apply on all existing monitors", | 
				
			|||
    backupDescription: "You can backup all monitors and all notifications into a JSON file.", | 
				
			|||
    backupDescription2: "PS: History and event data is not included.", | 
				
			|||
    backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", | 
				
			|||
    alertNoFile: "Please select a file to import.", | 
				
			|||
    alertWrongFileType: "Please select a JSON file.", | 
				
			|||
    "Clear all statistics": "Clear all Statistics", | 
				
			|||
    "Skip existing": "Skip existing", | 
				
			|||
    Overwrite: "Overwrite", | 
				
			|||
    Options: "Options", | 
				
			|||
    "Keep both": "Keep both", | 
				
			|||
    "Verify Token": "Verify Token", | 
				
			|||
    "Setup 2FA": "Setup 2FA", | 
				
			|||
    "Enable 2FA": "Enable 2FA", | 
				
			|||
    "Disable 2FA": "Disable 2FA", | 
				
			|||
    "2FA Settings": "2FA Settings", | 
				
			|||
    "Two Factor Authentication": "Two Factor Authentication", | 
				
			|||
    Active: "Active", | 
				
			|||
    Inactive: "Inactive", | 
				
			|||
    Token: "Token", | 
				
			|||
    "Show URI": "Show URI", | 
				
			|||
    Tags: "Tags", | 
				
			|||
    "Add New below or Select...": "Add New below or Select...", | 
				
			|||
    "Tag with this name already exist.": "Tag with this name already exist.", | 
				
			|||
    "Tag with this value already exist.": "Tag with this value already exist.", | 
				
			|||
    color: "color", | 
				
			|||
    "value (optional)": "value (optional)", | 
				
			|||
    Gray: "Gray", | 
				
			|||
    Red: "Red", | 
				
			|||
    Orange: "Orange", | 
				
			|||
    Green: "Green", | 
				
			|||
    Blue: "Blue", | 
				
			|||
    Indigo: "Indigo", | 
				
			|||
    Purple: "Purple", | 
				
			|||
    Pink: "Pink", | 
				
			|||
    "Search...": "Search...", | 
				
			|||
    "Avg. Ping": "Avg. Ping", | 
				
			|||
    "Avg. Response": "Avg. Response", | 
				
			|||
    "Entry Page": "Entry Page", | 
				
			|||
    "statusPageNothing": "Nothing here, please add a group or a monitor.", | 
				
			|||
    "No Services": "No Services", | 
				
			|||
    "All Systems Operational": "All Systems Operational", | 
				
			|||
    "Partially Degraded Service": "Partially Degraded Service", | 
				
			|||
    "Degraded Service": "Degraded Service", | 
				
			|||
    "Add Group": "Add Group", | 
				
			|||
    "Add a monitor": "Add a monitor", | 
				
			|||
    "Edit Status Page": "Edit Status Page", | 
				
			|||
    "Go to Dashboard": "Go to Dashboard", | 
				
			|||
}; | 
				
			|||
@ -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"); | 
				
			|||
        } else { | 
				
			|||
            this.$router.push("/dashboard"); | 
				
			|||
        } | 
				
			|||
    }, | 
				
			|||
 | 
				
			|||
}; | 
				
			|||
</script> | 
				
			|||
@ -0,0 +1,653 @@ | 
				
			|||
<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" /> | 
				
			|||
                    {{ $t("Edit Status Page") }} | 
				
			|||
                </button> | 
				
			|||
 | 
				
			|||
                <a href="/dashboard" class="btn btn-info"> | 
				
			|||
                    <font-awesome-icon icon="tachometer-alt" /> | 
				
			|||
                    {{ $t("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" /> | 
				
			|||
                {{ $t("No Services") }} | 
				
			|||
            </div> | 
				
			|||
 | 
				
			|||
            <template v-else> | 
				
			|||
                <div v-if="allUp"> | 
				
			|||
                    <font-awesome-icon icon="check-circle" class="ok" /> | 
				
			|||
                    {{ $t("All Systems Operational") }} | 
				
			|||
                </div> | 
				
			|||
 | 
				
			|||
                <div v-else-if="partialDown"> | 
				
			|||
                    <font-awesome-icon icon="exclamation-circle" class="warning" /> | 
				
			|||
                    {{ $t("Partially Degraded Service") }} | 
				
			|||
                </div> | 
				
			|||
 | 
				
			|||
                <div v-else-if="allDown"> | 
				
			|||
                    <font-awesome-icon icon="times-circle" class="danger" /> | 
				
			|||
                    {{ $t("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" /> | 
				
			|||
                    {{ $t("Add Group") }} | 
				
			|||
                </button> | 
				
			|||
            </div> | 
				
			|||
 | 
				
			|||
            <div class="mt-3"> | 
				
			|||
                <div v-if="allMonitorList.length > 0 && loadedData"> | 
				
			|||
                    <label>{{ $t("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 v-else class="text-center"> | 
				
			|||
                    {{ $t("No monitors available.") }}  <router-link to="/add">{{ $t("Add one") }}</router-link> | 
				
			|||
                </div> | 
				
			|||
            </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. --> | 
				
			|||
                👀 {{ $t("statusPageNothing") }} | 
				
			|||
            </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 this.$root.storage()); | 
				
			|||
 | 
				
			|||
        // Browser change page | 
				
			|||
        // https://stackoverflow.com/questions/7317273/warn-user-before-leaving-web-page-with-unsaved-changes | 
				
			|||
        window.addEventListener("beforeunload", (e) => { | 
				
			|||
            if (this.editMode) { | 
				
			|||
                (e || window.event).returnValue = leavePageMsg; | 
				
			|||
                return leavePageMsg; | 
				
			|||
            } else { | 
				
			|||
                return null; | 
				
			|||
            } | 
				
			|||
        }); | 
				
			|||
 | 
				
			|||
        // Special handle for dev | 
				
			|||
        const env = process.env.NODE_ENV; | 
				
			|||
        if (env === "development" || localStorage.dev === "dev") { | 
				
			|||
            this.baseURL = location.protocol + "//" + location.hostname + ":3001"; | 
				
			|||
        } | 
				
			|||
    }, | 
				
			|||
    async mounted() { | 
				
			|||
        axios.get("/api/status-page/config").then((res) => { | 
				
			|||
            this.config = res.data; | 
				
			|||
 | 
				
			|||
            if (this.config.logo) { | 
				
			|||
                this.imgDataUrl = this.config.logo; | 
				
			|||
            } | 
				
			|||
        }); | 
				
			|||
 | 
				
			|||
        axios.get("/api/status-page/incident").then((res) => { | 
				
			|||
            if (res.data.ok) { | 
				
			|||
                this.incident = res.data.incident; | 
				
			|||
            } | 
				
			|||
        }); | 
				
			|||
 | 
				
			|||
        axios.get("/api/status-page/monitor-list").then((res) => { | 
				
			|||
            this.$root.publicGroupList = res.data; | 
				
			|||
        }); | 
				
			|||
 | 
				
			|||
        // 5mins a loop | 
				
			|||
        this.updateHeartbeatList(); | 
				
			|||
        feedInterval = setInterval(() => { | 
				
			|||
            this.updateHeartbeatList(); | 
				
			|||
        }, (300 + 10) * 1000); | 
				
			|||
    }, | 
				
			|||
    methods: { | 
				
			|||
 | 
				
			|||
        updateHeartbeatList() { | 
				
			|||
            // If editMode, it will use the data from websocket. | 
				
			|||
            if (! this.editMode) { | 
				
			|||
                axios.get("/api/status-page/heartbeat").then((res) => { | 
				
			|||
                    this.$root.heartbeatList = res.data.heartbeatList; | 
				
			|||
                    this.$root.uptimeList = res.data.uptimeList; | 
				
			|||
                    this.loadedData = true; | 
				
			|||
                }); | 
				
			|||
            } | 
				
			|||
        }, | 
				
			|||
 | 
				
			|||
        edit() { | 
				
			|||
            this.$root.initSocketIO(true); | 
				
			|||
            this.enableEditMode = true; | 
				
			|||
        }, | 
				
			|||
 | 
				
			|||
        save() { | 
				
			|||
            this.$root.getSocket().emit("saveStatusPage", this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => { | 
				
			|||
                if (res.ok) { | 
				
			|||
                    this.enableEditMode = false; | 
				
			|||
                    this.$root.publicGroupList = res.publicGroupList; | 
				
			|||
                    location.reload(); | 
				
			|||
                } else { | 
				
			|||
                    toast.error(res.msg); | 
				
			|||
                } | 
				
			|||
            }); | 
				
			|||
        }, | 
				
			|||
 | 
				
			|||
        monitorSelectorLabel(monitor) { | 
				
			|||
            return `${monitor.name}`; | 
				
			|||
        }, | 
				
			|||
 | 
				
			|||
        addGroup() { | 
				
			|||
            let groupName = "Untitled Group"; | 
				
			|||
 | 
				
			|||
            if (this.$root.publicGroupList.length === 0) { | 
				
			|||
                groupName = "Services"; | 
				
			|||
            } | 
				
			|||
 | 
				
			|||
            this.$root.publicGroupList.push({ | 
				
			|||
                name: groupName, | 
				
			|||
                monitorList: [], | 
				
			|||
            }); | 
				
			|||
        }, | 
				
			|||
 | 
				
			|||
        discard() { | 
				
			|||
            location.reload(); | 
				
			|||
        }, | 
				
			|||
 | 
				
			|||
        changeTheme(name) { | 
				
			|||
            this.config.statusPageTheme = name; | 
				
			|||
        }, | 
				
			|||
 | 
				
			|||
        /** | 
				
			|||
         * Crop Success | 
				
			|||
         */ | 
				
			|||
        cropSuccess(imgDataUrl) { | 
				
			|||
            this.imgDataUrl = imgDataUrl; | 
				
			|||
        }, | 
				
			|||
 | 
				
			|||
        showImageCropUploadMethod() { | 
				
			|||
            if (this.editMode) { | 
				
			|||
                this.showImageCropUpload = true; | 
				
			|||
            } | 
				
			|||
        }, | 
				
			|||
 | 
				
			|||
        createIncident() { | 
				
			|||
            this.enableEditIncidentMode = true; | 
				
			|||
 | 
				
			|||
            if (this.incident) { | 
				
			|||
                this.previousIncident = this.incident; | 
				
			|||
            } | 
				
			|||
 | 
				
			|||
            this.incident = { | 
				
			|||
                title: "", | 
				
			|||
                content: "", | 
				
			|||
                style: "primary", | 
				
			|||
            }; | 
				
			|||
        }, | 
				
			|||
 | 
				
			|||
        postIncident() { | 
				
			|||
            if (this.incident.title == "" || this.incident.content == "") { | 
				
			|||
                toast.error("Please input title and content."); | 
				
			|||
                return; | 
				
			|||
            } | 
				
			|||
 | 
				
			|||
            this.$root.getSocket().emit("postIncident", this.incident, (res) => { | 
				
			|||
 | 
				
			|||
                if (res.ok) { | 
				
			|||
                    this.enableEditIncidentMode = false; | 
				
			|||
                    this.incident = res.incident; | 
				
			|||
                } else { | 
				
			|||
                    toast.error(res.msg); | 
				
			|||
                } | 
				
			|||
 | 
				
			|||
            }); | 
				
			|||
 | 
				
			|||
        }, | 
				
			|||
 | 
				
			|||
        /** | 
				
			|||
         * Click Edit Button | 
				
			|||
         */ | 
				
			|||
        editIncident() { | 
				
			|||
            this.enableEditIncidentMode = true; | 
				
			|||
            this.previousIncident = Object.assign({}, this.incident); | 
				
			|||
        }, | 
				
			|||
 | 
				
			|||
        cancelIncident() { | 
				
			|||
            this.enableEditIncidentMode = false; | 
				
			|||
 | 
				
			|||
            if (this.previousIncident) { | 
				
			|||
                this.incident = this.previousIncident; | 
				
			|||
                this.previousIncident = null; | 
				
			|||
            } | 
				
			|||
        }, | 
				
			|||
 | 
				
			|||
        unpinIncident() { | 
				
			|||
            this.$root.getSocket().emit("unpinIncident", () => { | 
				
			|||
                this.incident = null; | 
				
			|||
            }); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
}; | 
				
			|||
</script> | 
				
			|||
 | 
				
			|||
<style lang="scss" scoped> | 
				
			|||
@import "../assets/vars.scss"; | 
				
			|||
 | 
				
			|||
.overall-status { | 
				
			|||
    font-weight: bold; | 
				
			|||
    font-size: 25px; | 
				
			|||
 | 
				
			|||
    .ok { | 
				
			|||
        color: $primary; | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    .warning { | 
				
			|||
        color: $warning; | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    .danger { | 
				
			|||
        color: $danger; | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
 | 
				
			|||
h1 { | 
				
			|||
    font-size: 30px; | 
				
			|||
 | 
				
			|||
    img { | 
				
			|||
        vertical-align: middle; | 
				
			|||
        height: 60px; | 
				
			|||
        width: 60px; | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
 | 
				
			|||
footer { | 
				
			|||
    text-align: center; | 
				
			|||
    font-size: 14px; | 
				
			|||
} | 
				
			|||
 | 
				
			|||
.description span { | 
				
			|||
    min-width: 50px; | 
				
			|||
} | 
				
			|||
 | 
				
			|||
.logo-wrapper { | 
				
			|||
    display: inline-block; | 
				
			|||
    position: relative; | 
				
			|||
 | 
				
			|||
    &:hover { | 
				
			|||
        .icon-upload { | 
				
			|||
            transform: scale(1.2); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    .icon-upload { | 
				
			|||
        transition: all $easing-in 0.2s; | 
				
			|||
        position: absolute; | 
				
			|||
        bottom: 6px; | 
				
			|||
        font-size: 20px; | 
				
			|||
        left: -14px; | 
				
			|||
        background-color: white; | 
				
			|||
        padding: 5px; | 
				
			|||
        border-radius: 10px; | 
				
			|||
        cursor: pointer; | 
				
			|||
        box-shadow: 0 15px 70px rgba(0, 0, 0, 0.9); | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
 | 
				
			|||
.logo { | 
				
			|||
    transition: all $easing-in 0.2s; | 
				
			|||
 | 
				
			|||
    &.edit-mode { | 
				
			|||
        cursor: pointer; | 
				
			|||
 | 
				
			|||
        &:hover { | 
				
			|||
            transform: scale(1.2); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
 | 
				
			|||
.incident { | 
				
			|||
    .content { | 
				
			|||
        &[contenteditable=true] { | 
				
			|||
            min-height: 60px; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    .date { | 
				
			|||
        font-size: 12px; | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
 | 
				
			|||
.mobile { | 
				
			|||
    h1 { | 
				
			|||
        font-size: 22px; | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    .overall-status { | 
				
			|||
        font-size: 20px; | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
 | 
				
			|||
</style> | 
				
			|||
@ -0,0 +1,85 @@ | 
				
			|||
import { createRouter, createWebHistory } from "vue-router"; | 
				
			|||
import EmptyLayout from "./layouts/EmptyLayout.vue"; | 
				
			|||
import Layout from "./layouts/Layout.vue"; | 
				
			|||
import Dashboard from "./pages/Dashboard.vue"; | 
				
			|||
import DashboardHome from "./pages/DashboardHome.vue"; | 
				
			|||
import Details from "./pages/Details.vue"; | 
				
			|||
import EditMonitor from "./pages/EditMonitor.vue"; | 
				
			|||
import List from "./pages/List.vue"; | 
				
			|||
import Settings from "./pages/Settings.vue"; | 
				
			|||
import Setup from "./pages/Setup.vue"; | 
				
			|||
import StatusPage from "./pages/StatusPage.vue"; | 
				
			|||
import Entry from "./pages/Entry.vue"; | 
				
			|||
 | 
				
			|||
const routes = [ | 
				
			|||
    { | 
				
			|||
        path: "/", | 
				
			|||
        component: Entry, | 
				
			|||
    }, | 
				
			|||
    { | 
				
			|||
        // If it is "/dashboard", the active link is not working
 | 
				
			|||
        // If it is "", it overrides the "/" unexpectedly
 | 
				
			|||
        // Give a random name to solve the problem.
 | 
				
			|||
        path: "/empty", | 
				
			|||
        component: Layout, | 
				
			|||
        children: [ | 
				
			|||
            { | 
				
			|||
                path: "", | 
				
			|||
                component: Dashboard, | 
				
			|||
                children: [ | 
				
			|||
                    { | 
				
			|||
                        name: "DashboardHome", | 
				
			|||
                        path: "/dashboard", | 
				
			|||
                        component: DashboardHome, | 
				
			|||
                        children: [ | 
				
			|||
                            { | 
				
			|||
                                path: "/dashboard/:id", | 
				
			|||
                                component: EmptyLayout, | 
				
			|||
                                children: [ | 
				
			|||
                                    { | 
				
			|||
                                        path: "", | 
				
			|||
                                        component: Details, | 
				
			|||
                                    }, | 
				
			|||
                                    { | 
				
			|||
                                        path: "/edit/:id", | 
				
			|||
                                        component: EditMonitor, | 
				
			|||
                                    }, | 
				
			|||
                                ], | 
				
			|||
                            }, | 
				
			|||
                            { | 
				
			|||
                                path: "/add", | 
				
			|||
                                component: EditMonitor, | 
				
			|||
                            }, | 
				
			|||
                            { | 
				
			|||
                                path: "/list", | 
				
			|||
                                component: List, | 
				
			|||
                            }, | 
				
			|||
                        ], | 
				
			|||
                    }, | 
				
			|||
                    { | 
				
			|||
                        path: "/settings", | 
				
			|||
                        component: Settings, | 
				
			|||
                    }, | 
				
			|||
                ], | 
				
			|||
            }, | 
				
			|||
        ], | 
				
			|||
    }, | 
				
			|||
    { | 
				
			|||
        path: "/setup", | 
				
			|||
        component: Setup, | 
				
			|||
    }, | 
				
			|||
    { | 
				
			|||
        path: "/status-page", | 
				
			|||
        component: StatusPage, | 
				
			|||
    }, | 
				
			|||
    { | 
				
			|||
        path: "/status", | 
				
			|||
        component: StatusPage, | 
				
			|||
    }, | 
				
			|||
]; | 
				
			|||
 | 
				
			|||
export const router = createRouter({ | 
				
			|||
    linkActiveClass: "active", | 
				
			|||
    history: createWebHistory(), | 
				
			|||
    routes, | 
				
			|||
}); | 
				
			|||
@ -1,70 +1,104 @@ | 
				
			|||
"use strict"; | 
				
			|||
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; | 
				
			|||
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() { | 
				
			|||
    if (!String.prototype.replaceAll) { | 
				
			|||
        String.prototype.replaceAll = function (str, newStr) { | 
				
			|||
            if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") { | 
				
			|||
                return this.replace(str, newStr); | 
				
			|||
            } | 
				
			|||
            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; | 
				
			|||
function getRandomArbitrary(min, max) { | 
				
			|||
    return Math.random() * (max - min) + min; | 
				
			|||
} | 
				
			|||
exports.getRandomArbitrary = getRandomArbitrary; | 
				
			|||
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; | 
				
			|||
 | 
				
			|||
@ -0,0 +1,10 @@ | 
				
			|||
FROM ubuntu | 
				
			|||
WORKDIR /app | 
				
			|||
RUN apt update && apt --yes install git curl | 
				
			|||
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - | 
				
			|||
RUN apt --yes install nodejs | 
				
			|||
RUN git clone https://github.com/louislam/uptime-kuma.git . | 
				
			|||
RUN npm run setup | 
				
			|||
 | 
				
			|||
# Option 1. Try it | 
				
			|||
RUN node server/server.js | 
				
			|||
					Loading…
					
					
				
		Reference in new issue