zsxeee
3 years ago
71 changed files with 5063 additions and 3300 deletions
Binary file not shown.
@ -0,0 +1,30 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
BEGIN TRANSACTION; |
|||
|
|||
create table `group` |
|||
( |
|||
id INTEGER not null |
|||
constraint group_pk |
|||
primary key autoincrement, |
|||
name VARCHAR(255) not null, |
|||
created_date DATETIME default (DATETIME('now')) not null, |
|||
public BOOLEAN default 0 not null, |
|||
active BOOLEAN default 1 not null, |
|||
weight BOOLEAN NOT NULL DEFAULT 1000 |
|||
); |
|||
|
|||
CREATE TABLE [monitor_group] |
|||
( |
|||
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, |
|||
[monitor_id] INTEGER NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE, |
|||
[group_id] INTEGER NOT NULL REFERENCES [group] ([id]) ON DELETE CASCADE ON UPDATE CASCADE, |
|||
weight BOOLEAN NOT NULL DEFAULT 1000 |
|||
); |
|||
|
|||
CREATE INDEX [fk] |
|||
ON [monitor_group] ( |
|||
[monitor_id], |
|||
[group_id]); |
|||
|
|||
|
|||
COMMIT; |
@ -0,0 +1,18 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
BEGIN TRANSACTION; |
|||
|
|||
create table incident |
|||
( |
|||
id INTEGER not null |
|||
constraint incident_pk |
|||
primary key autoincrement, |
|||
title VARCHAR(255) not null, |
|||
content TEXT not null, |
|||
style VARCHAR(30) default 'warning' not null, |
|||
created_date DATETIME default (DATETIME('now')) not null, |
|||
last_updated_date DATETIME, |
|||
pin BOOLEAN default 1 not null, |
|||
active BOOLEAN default 1 not null |
|||
); |
|||
|
|||
COMMIT; |
File diff suppressed because it is too large
@ -0,0 +1,57 @@ |
|||
/* |
|||
From https://github.com/DiegoZoracKy/image-data-uri/blob/master/lib/image-data-uri.js
|
|||
Modified with 0 dependencies |
|||
*/ |
|||
let fs = require("fs"); |
|||
|
|||
let ImageDataURI = (() => { |
|||
|
|||
function decode(dataURI) { |
|||
if (!/data:image\//.test(dataURI)) { |
|||
console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\""); |
|||
return null; |
|||
} |
|||
|
|||
let regExMatches = dataURI.match("data:(image/.*);base64,(.*)"); |
|||
return { |
|||
imageType: regExMatches[1], |
|||
dataBase64: regExMatches[2], |
|||
dataBuffer: new Buffer(regExMatches[2], "base64") |
|||
}; |
|||
} |
|||
|
|||
function encode(data, mediaType) { |
|||
if (!data || !mediaType) { |
|||
console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType "); |
|||
return null; |
|||
} |
|||
|
|||
mediaType = (/\//.test(mediaType)) ? mediaType : "image/" + mediaType; |
|||
let dataBase64 = (Buffer.isBuffer(data)) ? data.toString("base64") : new Buffer(data).toString("base64"); |
|||
let dataImgBase64 = "data:" + mediaType + ";base64," + dataBase64; |
|||
|
|||
return dataImgBase64; |
|||
} |
|||
|
|||
function outputFile(dataURI, filePath) { |
|||
filePath = filePath || "./"; |
|||
return new Promise((resolve, reject) => { |
|||
let imageDecoded = decode(dataURI); |
|||
|
|||
fs.writeFile(filePath, imageDecoded.dataBuffer, err => { |
|||
if (err) { |
|||
return reject("ImageDataURI :: Error :: " + JSON.stringify(err, null, 4)); |
|||
} |
|||
resolve(filePath); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
decode: decode, |
|||
encode: encode, |
|||
outputFile: outputFile, |
|||
}; |
|||
})(); |
|||
|
|||
module.exports = ImageDataURI; |
@ -0,0 +1,34 @@ |
|||
const { BeanModel } = require("redbean-node/dist/bean-model"); |
|||
const { R } = require("redbean-node"); |
|||
|
|||
class Group extends BeanModel { |
|||
|
|||
async toPublicJSON() { |
|||
let monitorBeanList = await this.getMonitorList(); |
|||
let monitorList = []; |
|||
|
|||
for (let bean of monitorBeanList) { |
|||
monitorList.push(await bean.toPublicJSON()); |
|||
} |
|||
|
|||
return { |
|||
id: this.id, |
|||
name: this.name, |
|||
weight: this.weight, |
|||
monitorList, |
|||
}; |
|||
} |
|||
|
|||
async getMonitorList() { |
|||
return R.convertToBeans("monitor", await R.getAll(` |
|||
SELECT monitor.* FROM monitor, monitor_group |
|||
WHERE monitor.id = monitor_group.monitor_id |
|||
AND group_id = ? |
|||
ORDER BY monitor_group.weight |
|||
`, [
|
|||
this.id, |
|||
])); |
|||
} |
|||
} |
|||
|
|||
module.exports = Group; |
@ -0,0 +1,18 @@ |
|||
const { BeanModel } = require("redbean-node/dist/bean-model"); |
|||
|
|||
class Incident extends BeanModel { |
|||
|
|||
toPublicJSON() { |
|||
return { |
|||
id: this.id, |
|||
style: this.style, |
|||
title: this.title, |
|||
content: this.content, |
|||
pin: this.pin, |
|||
createdDate: this.createdDate, |
|||
lastUpdatedDate: this.lastUpdatedDate, |
|||
}; |
|||
} |
|||
} |
|||
|
|||
module.exports = Incident; |
@ -0,0 +1,749 @@ |
|||
let url = require("url"); |
|||
let MemoryCache = require("./memory-cache"); |
|||
|
|||
let t = { |
|||
ms: 1, |
|||
second: 1000, |
|||
minute: 60000, |
|||
hour: 3600000, |
|||
day: 3600000 * 24, |
|||
week: 3600000 * 24 * 7, |
|||
month: 3600000 * 24 * 30, |
|||
}; |
|||
|
|||
let instances = []; |
|||
|
|||
let matches = function (a) { |
|||
return function (b) { |
|||
return a === b; |
|||
}; |
|||
}; |
|||
|
|||
let doesntMatch = function (a) { |
|||
return function (b) { |
|||
return !matches(a)(b); |
|||
}; |
|||
}; |
|||
|
|||
let logDuration = function (d, prefix) { |
|||
let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms"; |
|||
return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m"; |
|||
}; |
|||
|
|||
function getSafeHeaders(res) { |
|||
return res.getHeaders ? res.getHeaders() : res._headers; |
|||
} |
|||
|
|||
function ApiCache() { |
|||
let memCache = new MemoryCache(); |
|||
|
|||
let globalOptions = { |
|||
debug: false, |
|||
defaultDuration: 3600000, |
|||
enabled: true, |
|||
appendKey: [], |
|||
jsonp: false, |
|||
redisClient: false, |
|||
headerBlacklist: [], |
|||
statusCodes: { |
|||
include: [], |
|||
exclude: [], |
|||
}, |
|||
events: { |
|||
expire: undefined, |
|||
}, |
|||
headers: { |
|||
// 'cache-control': 'no-cache' // example of header overwrite
|
|||
}, |
|||
trackPerformance: false, |
|||
respectCacheControl: false, |
|||
}; |
|||
|
|||
let middlewareOptions = []; |
|||
let instance = this; |
|||
let index = null; |
|||
let timers = {}; |
|||
let performanceArray = []; // for tracking cache hit rate
|
|||
|
|||
instances.push(this); |
|||
this.id = instances.length; |
|||
|
|||
function debug(a, b, c, d) { |
|||
let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) { |
|||
return arg !== undefined; |
|||
}); |
|||
let debugEnv = process.env.DEBUG && process.env.DEBUG.split(",").indexOf("apicache") !== -1; |
|||
|
|||
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr); |
|||
} |
|||
|
|||
function shouldCacheResponse(request, response, toggle) { |
|||
let opt = globalOptions; |
|||
let codes = opt.statusCodes; |
|||
|
|||
if (!response) { |
|||
return false; |
|||
} |
|||
|
|||
if (toggle && !toggle(request, response)) { |
|||
return false; |
|||
} |
|||
|
|||
if (codes.exclude && codes.exclude.length && codes.exclude.indexOf(response.statusCode) !== -1) { |
|||
return false; |
|||
} |
|||
if (codes.include && codes.include.length && codes.include.indexOf(response.statusCode) === -1) { |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
function addIndexEntries(key, req) { |
|||
let groupName = req.apicacheGroup; |
|||
|
|||
if (groupName) { |
|||
debug("group detected \"" + groupName + "\""); |
|||
let group = (index.groups[groupName] = index.groups[groupName] || []); |
|||
group.unshift(key); |
|||
} |
|||
|
|||
index.all.unshift(key); |
|||
} |
|||
|
|||
function filterBlacklistedHeaders(headers) { |
|||
return Object.keys(headers) |
|||
.filter(function (key) { |
|||
return globalOptions.headerBlacklist.indexOf(key) === -1; |
|||
}) |
|||
.reduce(function (acc, header) { |
|||
acc[header] = headers[header]; |
|||
return acc; |
|||
}, {}); |
|||
} |
|||
|
|||
function createCacheObject(status, headers, data, encoding) { |
|||
return { |
|||
status: status, |
|||
headers: filterBlacklistedHeaders(headers), |
|||
data: data, |
|||
encoding: encoding, |
|||
timestamp: new Date().getTime() / 1000, // seconds since epoch. This is used to properly decrement max-age headers in cached responses.
|
|||
}; |
|||
} |
|||
|
|||
function cacheResponse(key, value, duration) { |
|||
let redis = globalOptions.redisClient; |
|||
let expireCallback = globalOptions.events.expire; |
|||
|
|||
if (redis && redis.connected) { |
|||
try { |
|||
redis.hset(key, "response", JSON.stringify(value)); |
|||
redis.hset(key, "duration", duration); |
|||
redis.expire(key, duration / 1000, expireCallback || function () {}); |
|||
} catch (err) { |
|||
debug("[apicache] error in redis.hset()"); |
|||
} |
|||
} else { |
|||
memCache.add(key, value, duration, expireCallback); |
|||
} |
|||
|
|||
// add automatic cache clearing from duration, includes max limit on setTimeout
|
|||
timers[key] = setTimeout(function () { |
|||
instance.clear(key, true); |
|||
}, Math.min(duration, 2147483647)); |
|||
} |
|||
|
|||
function accumulateContent(res, content) { |
|||
if (content) { |
|||
if (typeof content == "string") { |
|||
res._apicache.content = (res._apicache.content || "") + content; |
|||
} else if (Buffer.isBuffer(content)) { |
|||
let oldContent = res._apicache.content; |
|||
|
|||
if (typeof oldContent === "string") { |
|||
oldContent = !Buffer.from ? new Buffer(oldContent) : Buffer.from(oldContent); |
|||
} |
|||
|
|||
if (!oldContent) { |
|||
oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0); |
|||
} |
|||
|
|||
res._apicache.content = Buffer.concat( |
|||
[oldContent, content], |
|||
oldContent.length + content.length |
|||
); |
|||
} else { |
|||
res._apicache.content = content; |
|||
} |
|||
} |
|||
} |
|||
|
|||
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) { |
|||
// monkeypatch res.end to create cache object
|
|||
res._apicache = { |
|||
write: res.write, |
|||
writeHead: res.writeHead, |
|||
end: res.end, |
|||
cacheable: true, |
|||
content: undefined, |
|||
}; |
|||
|
|||
// append header overwrites if applicable
|
|||
Object.keys(globalOptions.headers).forEach(function (name) { |
|||
res.setHeader(name, globalOptions.headers[name]); |
|||
}); |
|||
|
|||
res.writeHead = function () { |
|||
// add cache control headers
|
|||
if (!globalOptions.headers["cache-control"]) { |
|||
if (shouldCacheResponse(req, res, toggle)) { |
|||
res.setHeader("cache-control", "max-age=" + (duration / 1000).toFixed(0)); |
|||
} else { |
|||
res.setHeader("cache-control", "no-cache, no-store, must-revalidate"); |
|||
} |
|||
} |
|||
|
|||
res._apicache.headers = Object.assign({}, getSafeHeaders(res)); |
|||
return res._apicache.writeHead.apply(this, arguments); |
|||
}; |
|||
|
|||
// patch res.write
|
|||
res.write = function (content) { |
|||
accumulateContent(res, content); |
|||
return res._apicache.write.apply(this, arguments); |
|||
}; |
|||
|
|||
// patch res.end
|
|||
res.end = function (content, encoding) { |
|||
if (shouldCacheResponse(req, res, toggle)) { |
|||
accumulateContent(res, content); |
|||
|
|||
if (res._apicache.cacheable && res._apicache.content) { |
|||
addIndexEntries(key, req); |
|||
let headers = res._apicache.headers || getSafeHeaders(res); |
|||
let cacheObject = createCacheObject( |
|||
res.statusCode, |
|||
headers, |
|||
res._apicache.content, |
|||
encoding |
|||
); |
|||
cacheResponse(key, cacheObject, duration); |
|||
|
|||
// display log entry
|
|||
let elapsed = new Date() - req.apicacheTimer; |
|||
debug("adding cache entry for \"" + key + "\" @ " + strDuration, logDuration(elapsed)); |
|||
debug("_apicache.headers: ", res._apicache.headers); |
|||
debug("res.getHeaders(): ", getSafeHeaders(res)); |
|||
debug("cacheObject: ", cacheObject); |
|||
} |
|||
} |
|||
|
|||
return res._apicache.end.apply(this, arguments); |
|||
}; |
|||
|
|||
next(); |
|||
} |
|||
|
|||
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) { |
|||
if (toggle && !toggle(request, response)) { |
|||
return next(); |
|||
} |
|||
|
|||
let headers = getSafeHeaders(response); |
|||
|
|||
// Modified by @louislam, removed Cache-control, since I don't need client side cache!
|
|||
// Original Source: https://github.com/kwhitley/apicache/blob/0d5686cc21fad353c6dddee646288c2fca3e4f50/src/apicache.js#L254
|
|||
Object.assign(headers, filterBlacklistedHeaders(cacheObject.headers || {})); |
|||
|
|||
// only embed apicache headers when not in production environment
|
|||
if (process.env.NODE_ENV !== "production") { |
|||
Object.assign(headers, { |
|||
"apicache-store": globalOptions.redisClient ? "redis" : "memory", |
|||
"apicache-version": "1.6.2-modified", |
|||
}); |
|||
} |
|||
|
|||
// unstringify buffers
|
|||
let data = cacheObject.data; |
|||
if (data && data.type === "Buffer") { |
|||
data = |
|||
typeof data.data === "number" ? new Buffer.alloc(data.data) : new Buffer.from(data.data); |
|||
} |
|||
|
|||
// test Etag against If-None-Match for 304
|
|||
let cachedEtag = cacheObject.headers.etag; |
|||
let requestEtag = request.headers["if-none-match"]; |
|||
|
|||
if (requestEtag && cachedEtag === requestEtag) { |
|||
response.writeHead(304, headers); |
|||
return response.end(); |
|||
} |
|||
|
|||
response.writeHead(cacheObject.status || 200, headers); |
|||
|
|||
return response.end(data, cacheObject.encoding); |
|||
} |
|||
|
|||
function syncOptions() { |
|||
for (let i in middlewareOptions) { |
|||
Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions); |
|||
} |
|||
} |
|||
|
|||
this.clear = function (target, isAutomatic) { |
|||
let group = index.groups[target]; |
|||
let redis = globalOptions.redisClient; |
|||
|
|||
if (group) { |
|||
debug("clearing group \"" + target + "\""); |
|||
|
|||
group.forEach(function (key) { |
|||
debug("clearing cached entry for \"" + key + "\""); |
|||
clearTimeout(timers[key]); |
|||
delete timers[key]; |
|||
if (!globalOptions.redisClient) { |
|||
memCache.delete(key); |
|||
} else { |
|||
try { |
|||
redis.del(key); |
|||
} catch (err) { |
|||
console.log("[apicache] error in redis.del(\"" + key + "\")"); |
|||
} |
|||
} |
|||
index.all = index.all.filter(doesntMatch(key)); |
|||
}); |
|||
|
|||
delete index.groups[target]; |
|||
} else if (target) { |
|||
debug("clearing " + (isAutomatic ? "expired" : "cached") + " entry for \"" + target + "\""); |
|||
clearTimeout(timers[target]); |
|||
delete timers[target]; |
|||
// clear actual cached entry
|
|||
if (!redis) { |
|||
memCache.delete(target); |
|||
} else { |
|||
try { |
|||
redis.del(target); |
|||
} catch (err) { |
|||
console.log("[apicache] error in redis.del(\"" + target + "\")"); |
|||
} |
|||
} |
|||
|
|||
// remove from global index
|
|||
index.all = index.all.filter(doesntMatch(target)); |
|||
|
|||
// remove target from each group that it may exist in
|
|||
Object.keys(index.groups).forEach(function (groupName) { |
|||
index.groups[groupName] = index.groups[groupName].filter(doesntMatch(target)); |
|||
|
|||
// delete group if now empty
|
|||
if (!index.groups[groupName].length) { |
|||
delete index.groups[groupName]; |
|||
} |
|||
}); |
|||
} else { |
|||
debug("clearing entire index"); |
|||
|
|||
if (!redis) { |
|||
memCache.clear(); |
|||
} else { |
|||
// clear redis keys one by one from internal index to prevent clearing non-apicache entries
|
|||
index.all.forEach(function (key) { |
|||
clearTimeout(timers[key]); |
|||
delete timers[key]; |
|||
try { |
|||
redis.del(key); |
|||
} catch (err) { |
|||
console.log("[apicache] error in redis.del(\"" + key + "\")"); |
|||
} |
|||
}); |
|||
} |
|||
this.resetIndex(); |
|||
} |
|||
|
|||
return this.getIndex(); |
|||
}; |
|||
|
|||
function parseDuration(duration, defaultDuration) { |
|||
if (typeof duration === "number") { |
|||
return duration; |
|||
} |
|||
|
|||
if (typeof duration === "string") { |
|||
let split = duration.match(/^([\d\.,]+)\s?(\w+)$/); |
|||
|
|||
if (split.length === 3) { |
|||
let len = parseFloat(split[1]); |
|||
let unit = split[2].replace(/s$/i, "").toLowerCase(); |
|||
if (unit === "m") { |
|||
unit = "ms"; |
|||
} |
|||
|
|||
return (len || 1) * (t[unit] || 0); |
|||
} |
|||
} |
|||
|
|||
return defaultDuration; |
|||
} |
|||
|
|||
this.getDuration = function (duration) { |
|||
return parseDuration(duration, globalOptions.defaultDuration); |
|||
}; |
|||
|
|||
/** |
|||
* Return cache performance statistics (hit rate). Suitable for putting into a route: |
|||
* <code> |
|||
* app.get('/api/cache/performance', (req, res) => { |
|||
* res.json(apicache.getPerformance()) |
|||
* }) |
|||
* </code> |
|||
*/ |
|||
this.getPerformance = function () { |
|||
return performanceArray.map(function (p) { |
|||
return p.report(); |
|||
}); |
|||
}; |
|||
|
|||
this.getIndex = function (group) { |
|||
if (group) { |
|||
return index.groups[group]; |
|||
} else { |
|||
return index; |
|||
} |
|||
}; |
|||
|
|||
this.middleware = function cache(strDuration, middlewareToggle, localOptions) { |
|||
let duration = instance.getDuration(strDuration); |
|||
let opt = {}; |
|||
|
|||
middlewareOptions.push({ |
|||
options: opt, |
|||
}); |
|||
|
|||
let options = function (localOptions) { |
|||
if (localOptions) { |
|||
middlewareOptions.find(function (middleware) { |
|||
return middleware.options === opt; |
|||
}).localOptions = localOptions; |
|||
} |
|||
|
|||
syncOptions(); |
|||
|
|||
return opt; |
|||
}; |
|||
|
|||
options(localOptions); |
|||
|
|||
/** |
|||
* A Function for non tracking performance |
|||
*/ |
|||
function NOOPCachePerformance() { |
|||
this.report = this.hit = this.miss = function () {}; // noop;
|
|||
} |
|||
|
|||
/** |
|||
* A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above. |
|||
*/ |
|||
function CachePerformance() { |
|||
/** |
|||
* Tracks the hit rate for the last 100 requests. |
|||
* If there have been fewer than 100 requests, the hit rate just considers the requests that have happened. |
|||
*/ |
|||
this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
|
|||
|
|||
/** |
|||
* Tracks the hit rate for the last 1000 requests. |
|||
* If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened. |
|||
*/ |
|||
this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
|
|||
|
|||
/** |
|||
* Tracks the hit rate for the last 10000 requests. |
|||
* If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened. |
|||
*/ |
|||
this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
|
|||
|
|||
/** |
|||
* Tracks the hit rate for the last 100000 requests. |
|||
* If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened. |
|||
*/ |
|||
this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
|
|||
|
|||
/** |
|||
* The number of calls that have passed through the middleware since the server started. |
|||
*/ |
|||
this.callCount = 0; |
|||
|
|||
/** |
|||
* The total number of hits since the server started |
|||
*/ |
|||
this.hitCount = 0; |
|||
|
|||
/** |
|||
* The key from the last cache hit. This is useful in identifying which route these statistics apply to. |
|||
*/ |
|||
this.lastCacheHit = null; |
|||
|
|||
/** |
|||
* The key from the last cache miss. This is useful in identifying which route these statistics apply to. |
|||
*/ |
|||
this.lastCacheMiss = null; |
|||
|
|||
/** |
|||
* Return performance statistics |
|||
*/ |
|||
this.report = function () { |
|||
return { |
|||
lastCacheHit: this.lastCacheHit, |
|||
lastCacheMiss: this.lastCacheMiss, |
|||
callCount: this.callCount, |
|||
hitCount: this.hitCount, |
|||
missCount: this.callCount - this.hitCount, |
|||
hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount, |
|||
hitRateLast100: this.hitRate(this.hitsLast100), |
|||
hitRateLast1000: this.hitRate(this.hitsLast1000), |
|||
hitRateLast10000: this.hitRate(this.hitsLast10000), |
|||
hitRateLast100000: this.hitRate(this.hitsLast100000), |
|||
}; |
|||
}; |
|||
|
|||
/** |
|||
* Computes a cache hit rate from an array of hits and misses. |
|||
* @param {Uint8Array} array An array representing hits and misses. |
|||
* @returns a number between 0 and 1, or null if the array has no hits or misses |
|||
*/ |
|||
this.hitRate = function (array) { |
|||
let hits = 0; |
|||
let misses = 0; |
|||
for (let i = 0; i < array.length; i++) { |
|||
let n8 = array[i]; |
|||
for (let j = 0; j < 4; j++) { |
|||
switch (n8 & 3) { |
|||
case 1: |
|||
hits++; |
|||
break; |
|||
case 2: |
|||
misses++; |
|||
break; |
|||
} |
|||
n8 >>= 2; |
|||
} |
|||
} |
|||
let total = hits + misses; |
|||
if (total == 0) { |
|||
return null; |
|||
} |
|||
return hits / total; |
|||
}; |
|||
|
|||
/** |
|||
* Record a hit or miss in the given array. It will be recorded at a position determined |
|||
* by the current value of the callCount variable. |
|||
* @param {Uint8Array} array An array representing hits and misses. |
|||
* @param {boolean} hit true for a hit, false for a miss |
|||
* Each element in the array is 8 bits, and encodes 4 hit/miss records. |
|||
* Each hit or miss is encoded as to bits as follows: |
|||
* 00 means no hit or miss has been recorded in these bits |
|||
* 01 encodes a hit |
|||
* 10 encodes a miss |
|||
*/ |
|||
this.recordHitInArray = function (array, hit) { |
|||
let arrayIndex = ~~(this.callCount / 4) % array.length; |
|||
let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
|
|||
let clearMask = ~(3 << bitOffset); |
|||
let record = (hit ? 1 : 2) << bitOffset; |
|||
array[arrayIndex] = (array[arrayIndex] & clearMask) | record; |
|||
}; |
|||
|
|||
/** |
|||
* Records the hit or miss in the tracking arrays and increments the call count. |
|||
* @param {boolean} hit true records a hit, false records a miss |
|||
*/ |
|||
this.recordHit = function (hit) { |
|||
this.recordHitInArray(this.hitsLast100, hit); |
|||
this.recordHitInArray(this.hitsLast1000, hit); |
|||
this.recordHitInArray(this.hitsLast10000, hit); |
|||
this.recordHitInArray(this.hitsLast100000, hit); |
|||
if (hit) { |
|||
this.hitCount++; |
|||
} |
|||
this.callCount++; |
|||
}; |
|||
|
|||
/** |
|||
* Records a hit event, setting lastCacheMiss to the given key |
|||
* @param {string} key The key that had the cache hit |
|||
*/ |
|||
this.hit = function (key) { |
|||
this.recordHit(true); |
|||
this.lastCacheHit = key; |
|||
}; |
|||
|
|||
/** |
|||
* Records a miss event, setting lastCacheMiss to the given key |
|||
* @param {string} key The key that had the cache miss |
|||
*/ |
|||
this.miss = function (key) { |
|||
this.recordHit(false); |
|||
this.lastCacheMiss = key; |
|||
}; |
|||
} |
|||
|
|||
let perf = globalOptions.trackPerformance ? new CachePerformance() : new NOOPCachePerformance(); |
|||
|
|||
performanceArray.push(perf); |
|||
|
|||
let cache = function (req, res, next) { |
|||
function bypass() { |
|||
debug("bypass detected, skipping cache."); |
|||
return next(); |
|||
} |
|||
|
|||
// initial bypass chances
|
|||
if (!opt.enabled) { |
|||
return bypass(); |
|||
} |
|||
if ( |
|||
req.headers["x-apicache-bypass"] || |
|||
req.headers["x-apicache-force-fetch"] || |
|||
(opt.respectCacheControl && req.headers["cache-control"] == "no-cache") |
|||
) { |
|||
return bypass(); |
|||
} |
|||
|
|||
// REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER
|
|||
// if (typeof middlewareToggle === 'function') {
|
|||
// if (!middlewareToggle(req, res)) return bypass()
|
|||
// } else if (middlewareToggle !== undefined && !middlewareToggle) {
|
|||
// return bypass()
|
|||
// }
|
|||
|
|||
// embed timer
|
|||
req.apicacheTimer = new Date(); |
|||
|
|||
// In Express 4.x the url is ambigious based on where a router is mounted. originalUrl will give the full Url
|
|||
let key = req.originalUrl || req.url; |
|||
|
|||
// Remove querystring from key if jsonp option is enabled
|
|||
if (opt.jsonp) { |
|||
key = url.parse(key).pathname; |
|||
} |
|||
|
|||
// add appendKey (either custom function or response path)
|
|||
if (typeof opt.appendKey === "function") { |
|||
key += "$$appendKey=" + opt.appendKey(req, res); |
|||
} else if (opt.appendKey.length > 0) { |
|||
let appendKey = req; |
|||
|
|||
for (let i = 0; i < opt.appendKey.length; i++) { |
|||
appendKey = appendKey[opt.appendKey[i]]; |
|||
} |
|||
key += "$$appendKey=" + appendKey; |
|||
} |
|||
|
|||
// attempt cache hit
|
|||
let redis = opt.redisClient; |
|||
let cached = !redis ? memCache.getValue(key) : null; |
|||
|
|||
// send if cache hit from memory-cache
|
|||
if (cached) { |
|||
let elapsed = new Date() - req.apicacheTimer; |
|||
debug("sending cached (memory-cache) version of", key, logDuration(elapsed)); |
|||
|
|||
perf.hit(key); |
|||
return sendCachedResponse(req, res, cached, middlewareToggle, next, duration); |
|||
} |
|||
|
|||
// send if cache hit from redis
|
|||
if (redis && redis.connected) { |
|||
try { |
|||
redis.hgetall(key, function (err, obj) { |
|||
if (!err && obj && obj.response) { |
|||
let elapsed = new Date() - req.apicacheTimer; |
|||
debug("sending cached (redis) version of", key, logDuration(elapsed)); |
|||
|
|||
perf.hit(key); |
|||
return sendCachedResponse( |
|||
req, |
|||
res, |
|||
JSON.parse(obj.response), |
|||
middlewareToggle, |
|||
next, |
|||
duration |
|||
); |
|||
} else { |
|||
perf.miss(key); |
|||
return makeResponseCacheable( |
|||
req, |
|||
res, |
|||
next, |
|||
key, |
|||
duration, |
|||
strDuration, |
|||
middlewareToggle |
|||
); |
|||
} |
|||
}); |
|||
} catch (err) { |
|||
// bypass redis on error
|
|||
perf.miss(key); |
|||
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle); |
|||
} |
|||
} else { |
|||
perf.miss(key); |
|||
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle); |
|||
} |
|||
}; |
|||
|
|||
cache.options = options; |
|||
|
|||
return cache; |
|||
}; |
|||
|
|||
this.options = function (options) { |
|||
if (options) { |
|||
Object.assign(globalOptions, options); |
|||
syncOptions(); |
|||
|
|||
if ("defaultDuration" in options) { |
|||
// Convert the default duration to a number in milliseconds (if needed)
|
|||
globalOptions.defaultDuration = parseDuration(globalOptions.defaultDuration, 3600000); |
|||
} |
|||
|
|||
if (globalOptions.trackPerformance) { |
|||
debug("WARNING: using trackPerformance flag can cause high memory usage!"); |
|||
} |
|||
|
|||
return this; |
|||
} else { |
|||
return globalOptions; |
|||
} |
|||
}; |
|||
|
|||
this.resetIndex = function () { |
|||
index = { |
|||
all: [], |
|||
groups: {}, |
|||
}; |
|||
}; |
|||
|
|||
this.newInstance = function (config) { |
|||
let instance = new ApiCache(); |
|||
|
|||
if (config) { |
|||
instance.options(config); |
|||
} |
|||
|
|||
return instance; |
|||
}; |
|||
|
|||
this.clone = function () { |
|||
return this.newInstance(this.options()); |
|||
}; |
|||
|
|||
// initialize index
|
|||
this.resetIndex(); |
|||
} |
|||
|
|||
module.exports = new ApiCache(); |
@ -0,0 +1,14 @@ |
|||
const apicache = require("./apicache"); |
|||
|
|||
apicache.options({ |
|||
headerBlacklist: [ |
|||
"cache-control" |
|||
], |
|||
headers: { |
|||
// Disable client side cache, only server side cache.
|
|||
// BUG! Not working for the second request
|
|||
"cache-control": "no-cache", |
|||
}, |
|||
}); |
|||
|
|||
module.exports = apicache; |
@ -0,0 +1,59 @@ |
|||
function MemoryCache() { |
|||
this.cache = {}; |
|||
this.size = 0; |
|||
} |
|||
|
|||
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) { |
|||
let old = this.cache[key]; |
|||
let instance = this; |
|||
|
|||
let entry = { |
|||
value: value, |
|||
expire: time + Date.now(), |
|||
timeout: setTimeout(function () { |
|||
instance.delete(key); |
|||
return timeoutCallback && typeof timeoutCallback === "function" && timeoutCallback(value, key); |
|||
}, time) |
|||
}; |
|||
|
|||
this.cache[key] = entry; |
|||
this.size = Object.keys(this.cache).length; |
|||
|
|||
return entry; |
|||
}; |
|||
|
|||
MemoryCache.prototype.delete = function (key) { |
|||
let entry = this.cache[key]; |
|||
|
|||
if (entry) { |
|||
clearTimeout(entry.timeout); |
|||
} |
|||
|
|||
delete this.cache[key]; |
|||
|
|||
this.size = Object.keys(this.cache).length; |
|||
|
|||
return null; |
|||
}; |
|||
|
|||
MemoryCache.prototype.get = function (key) { |
|||
let entry = this.cache[key]; |
|||
|
|||
return entry; |
|||
}; |
|||
|
|||
MemoryCache.prototype.getValue = function (key) { |
|||
let entry = this.get(key); |
|||
|
|||
return entry && entry.value; |
|||
}; |
|||
|
|||
MemoryCache.prototype.clear = function () { |
|||
Object.keys(this.cache).forEach(function (key) { |
|||
this.delete(key); |
|||
}, this); |
|||
|
|||
return true; |
|||
}; |
|||
|
|||
module.exports = MemoryCache; |
@ -0,0 +1,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,73 @@ |
|||
@import "vars.scss"; |
|||
@import "node_modules/vue-multiselect/dist/vue-multiselect"; |
|||
|
|||
.multiselect__tags { |
|||
border-radius: 1.5rem; |
|||
border: 1px solid #ced4da; |
|||
min-height: 38px; |
|||
padding: 6px 40px 0 8px; |
|||
} |
|||
|
|||
.multiselect--active .multiselect__tags { |
|||
border-radius: 1rem; |
|||
} |
|||
|
|||
.multiselect__option--highlight { |
|||
background: $primary !important; |
|||
} |
|||
|
|||
.multiselect__option--highlight::after { |
|||
background: $primary !important; |
|||
} |
|||
|
|||
.multiselect__tag { |
|||
border-radius: 50rem; |
|||
margin-bottom: 0; |
|||
padding: 6px 26px 6px 10px; |
|||
background: $primary !important; |
|||
} |
|||
|
|||
.multiselect__placeholder { |
|||
font-size: 1rem; |
|||
padding-left: 6px; |
|||
padding-top: 0; |
|||
padding-bottom: 0; |
|||
margin-bottom: 0; |
|||
opacity: 0.67; |
|||
} |
|||
|
|||
.multiselect__input, |
|||
.multiselect__single { |
|||
line-height: 14px; |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
.dark { |
|||
.multiselect__tag { |
|||
color: $dark-font-color2; |
|||
} |
|||
|
|||
.multiselect__tags { |
|||
background-color: $dark-bg2; |
|||
border-color: $dark-border-color; |
|||
} |
|||
|
|||
.multiselect__input, |
|||
.multiselect__single { |
|||
background-color: $dark-bg2; |
|||
color: $dark-font-color; |
|||
} |
|||
|
|||
.multiselect__content-wrapper { |
|||
background-color: $dark-bg2; |
|||
border-color: $dark-border-color; |
|||
} |
|||
|
|||
.multiselect--above .multiselect__content-wrapper { |
|||
border-color: $dark-border-color; |
|||
} |
|||
|
|||
.multiselect__option--selected { |
|||
background-color: $dark-bg; |
|||
} |
|||
} |
@ -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,181 @@ |
|||
export default { |
|||
languageName: "Български", |
|||
checkEverySecond: "Проверявай на всеки {0} секунди.", |
|||
retryCheckEverySecond: "Повторен опит на всеки {0} секунди.", |
|||
retriesDescription: "Максимакен брой опити преди услугата да бъде маркирана като недостъпна и да бъде изпратено известие", |
|||
ignoreTLSError: "Игнорирай TLS/SSL грешки за HTTPS уебсайтове", |
|||
upsideDownModeDescription: "Обърни статуса от достъпен на недостъпен. Ако услугата е достъпна се вижда НЕДОСТЪПНА.", |
|||
maxRedirectDescription: "Максимален брой пренасочвания, които да бъдат следвани. Въведете 0 за да изключите пренасочване.", |
|||
acceptedStatusCodesDescription: "Изберете статус кодове, които се считат за успешен отговор.", |
|||
passwordNotMatchMsg: "Повторената парола не съвпада.", |
|||
notificationDescription: "Моля, задайте известието към монитор(и), за да функционира.", |
|||
keywordDescription: "Търсете ключова дума в обикновен html или JSON отговор - чувствителна е към регистъра", |
|||
pauseDashboardHome: "Пауза", |
|||
deleteMonitorMsg: "Наистина ли желаете да изтриете този монитор?", |
|||
deleteNotificationMsg: "Наистина ли желаете да изтриете известието за всички монитори?", |
|||
resoverserverDescription: "Cloudflare е сървърът по подразбиране, можете да промените сървъра по всяко време.", |
|||
rrtypeDescription: "Изберете ресурсния запис, който желаете да наблюдавате", |
|||
pauseMonitorMsg: "Наистина ли желаете да поставите в режим пауза?", |
|||
enableDefaultNotificationDescription: "За всеки нов монитор това известие ще бъде активирано по подразбиране. Можете да изключите известието за всеки отделен монитор.", |
|||
clearEventsMsg: "Наистина ли желаете да изтриете всички събития за този монитор?", |
|||
clearHeartbeatsMsg: "Наистина ли желаете да изтриете всички записи за честотни проверки на този монитор?", |
|||
confirmClearStatisticsMsg: "Наистина ли желаете да изтриете всички статистически данни?", |
|||
importHandleDescription: "Изберете 'Пропусни съществуващите', ако искате да пропуснете всеки монитор или известие със същото име. 'Презапис' ще изтрие всеки съществуващ монитор и известие.", |
|||
confirmImportMsg: "Сигурни ли сте за импортирането на архива? Моля, уверете се, че сте избрали правилната опция за импортиране.", |
|||
twoFAVerifyLabel: "Моля, въведете вашия токен код, за да проверите дали 2FA работи", |
|||
tokenValidSettingsMsg: "Токен кодът е валиден! Вече можете да запазите настройките за 2FA.", |
|||
confirmEnableTwoFAMsg: "Сигурни ли сте, че желаете да активирате 2FA?", |
|||
confirmDisableTwoFAMsg: "Сигурни ли сте, че желаете да изключите 2FA?", |
|||
Settings: "Настройки", |
|||
Dashboard: "Табло", |
|||
"New Update": "Нова актуализация", |
|||
Language: "Език", |
|||
Appearance: "Изглед", |
|||
Theme: "Тема", |
|||
General: "Общи", |
|||
Version: "Версия", |
|||
"Check Update On GitHub": "Провери за актуализация в GitHub", |
|||
List: "Списък", |
|||
Add: "Добави", |
|||
"Add New Monitor": "Добави монитор", |
|||
"Quick Stats": "Кратка статистика", |
|||
Up: "Достъпни", |
|||
Down: "Недостъпни", |
|||
Pending: "В изчакване", |
|||
Unknown: "Неизвестни", |
|||
Pause: "В пауза", |
|||
Name: "Име", |
|||
Status: "Статус", |
|||
DateTime: "Дата и час", |
|||
Message: "Съобщение", |
|||
"No important events": "Няма важни събития", |
|||
Resume: "Възобнови", |
|||
Edit: "Редактирай", |
|||
Delete: "Изтрий", |
|||
Current: "Текущ", |
|||
Uptime: "Време на работа", |
|||
"Cert Exp.": "Вал. сертификат", |
|||
days: "дни", |
|||
day: "ден", |
|||
"-day": "-ден", |
|||
hour: "час", |
|||
"-hour": "-час", |
|||
Response: "Отговор", |
|||
Ping: "Пинг", |
|||
"Monitor Type": "Монитор тип", |
|||
Keyword: "Ключова дума", |
|||
"Friendly Name": "Псевдоним", |
|||
URL: "URL Адрес", |
|||
Hostname: "Име на хост", |
|||
Port: "Порт", |
|||
"Heartbeat Interval": "Честота на проверка", |
|||
Retries: "Повторни опити", |
|||
"Heartbeat Retry Interval": "Честота на повторните опити", |
|||
Advanced: "Разширени", |
|||
"Upside Down Mode": "Обърнат режим", |
|||
"Max. Redirects": "Макс. брой пренасочвания", |
|||
"Accepted Status Codes": "Допустими статус кодове", |
|||
Save: "Запази", |
|||
Notifications: "Известявания", |
|||
"Not available, please setup.": "Не е налично. Моля, настройте.", |
|||
"Setup Notification": "Настройка за известяване", |
|||
Light: "Светла", |
|||
Dark: "Тъмна", |
|||
Auto: "Автоматично", |
|||
"Theme - Heartbeat Bar": "Тема - поле проверки", |
|||
Normal: "Нормално", |
|||
Bottom: "Долу", |
|||
None: "Без", |
|||
Timezone: "Часова зона", |
|||
"Search Engine Visibility": "Видимост за търсачки", |
|||
"Allow indexing": "Разреши индексиране", |
|||
"Discourage search engines from indexing site": "Обезкуражи индексирането на сайта от търсачките", |
|||
"Change Password": "Промени парола", |
|||
"Current Password": "Текуща парола", |
|||
"New Password": "Нова парола", |
|||
"Repeat New Password": "Повторете новата парола", |
|||
"Update Password": "Актуализирай парола", |
|||
"Disable Auth": "Изключи удостоверяване", |
|||
"Enable Auth": "Включи удостоверяване", |
|||
Logout: "Изход от профила", |
|||
Leave: "Напускам", |
|||
"I understand, please disable": "Разбирам. Моля, изключи", |
|||
Confirm: "Потвърди", |
|||
Yes: "Да", |
|||
No: "Не", |
|||
Username: "Потребител", |
|||
Password: "Парола", |
|||
"Remember me": "Запомни ме", |
|||
Login: "Вход", |
|||
"No Monitors, please": "Моля, без монитори", |
|||
"add one": "добави един", |
|||
"Notification Type": "Тип известяване", |
|||
Email: "Имейл", |
|||
Test: "Тест", |
|||
"Certificate Info": "Информация за сертификат", |
|||
"Resolver Server": "Преобразуващ (DNS) сървър", |
|||
"Resource Record Type": "Тип запис", |
|||
"Last Result": "Последен резултат", |
|||
"Create your admin account": "Създаване на администриращ акаунт", |
|||
"Repeat Password": "Повторете паролата", |
|||
"Import Backup": "Импорт на архив", |
|||
"Export Backup": "Експорт на архив", |
|||
Export: "Експорт", |
|||
Import: "Импорт", |
|||
respTime: "Време за отговор (ms)", |
|||
notAvailableShort: "Няма", |
|||
"Default enabled": "Включен по подразбиране", |
|||
"Apply on all existing monitors": "Приложи върху всички съществуващи монитори", |
|||
Create: "Създай", |
|||
"Clear Data": "Изчисти данни", |
|||
Events: "Събития", |
|||
Heartbeats: "Проверки", |
|||
"Auto Get": "Автоматияно получаване", |
|||
backupDescription: "Можете да архивирате всички монитори и всички известия в JSON файл.", |
|||
backupDescription2: "PS: Данни за история и събития не са включени.", |
|||
backupDescription3: "Чувствителни данни, като токен кодове за известяване, се съдържат в експортирания файл. Моля, бъдете внимателни с неговото съхранение.", |
|||
alertNoFile: "Моля, изберете файл за импортиране.", |
|||
alertWrongFileType: "Моля, изберете JSON файл.", |
|||
"Clear all statistics": "Изчисти всички статистики", |
|||
"Skip existing": "Пропусни съществуващите", |
|||
Overwrite: "Презапиши", |
|||
Options: "Опции", |
|||
"Keep both": "Запази двете", |
|||
"Verify Token": "Проверка на токен код", |
|||
"Setup 2FA": "Настройка 2FA", |
|||
"Enable 2FA": "Включи 2FA", |
|||
"Disable 2FA": "Изключи 2FA", |
|||
"2FA Settings": "Настройки 2FA", |
|||
"Two Factor Authentication": "Двуфакторно удостоверяване", |
|||
Active: "Активно", |
|||
Inactive: "Неактивно", |
|||
Token: "Токен код", |
|||
"Show URI": "Покажи URI", |
|||
Tags: "Етикети", |
|||
"Add New below or Select...": "Добави нов по-долу или избери...", |
|||
"Tag with this name already exist.": "Етикет с това име вече съществува.", |
|||
"Tag with this value already exist.": "Етикет с тази стойност вече съществува.", |
|||
color: "цвят", |
|||
"value (optional)": "стойност (по желание)", |
|||
Gray: "Сиво", |
|||
Red: "Червено", |
|||
Orange: "Оранжево", |
|||
Green: "Зелено", |
|||
Blue: "Синьо", |
|||
Indigo: "Индиго", |
|||
Purple: "Лилаво", |
|||
Pink: "Розово", |
|||
"Search...": "Търси...", |
|||
"Avg. Ping": "Ср. пинг", |
|||
"Avg. Response": "Ср. отговор", |
|||
"Entry Page": "Основна страница", |
|||
statusPageNothing: "Все още няма нищо тук. Моля, добавете група или монитор.", |
|||
"No Services": "Няма Услуги", |
|||
"All Systems Operational": "Всички системи функционират", |
|||
"Partially Degraded Service": "Частично влошена услуга", |
|||
"Degraded Service": "Влошена услуга", |
|||
"Add Group": "Добави група", |
|||
"Add a monitor": "Добави монитор", |
|||
"Edit Status Page": "Редактирай статус страница", |
|||
"Go to Dashboard": "Към Таблото", |
|||
}; |
@ -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,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,650 @@ |
|||
<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: {{ $root.datetime(incident.createdDate) }} ({{ dateFromNow(incident.createdDate) }})<br /> |
|||
<span v-if="incident.lastUpdatedDate"> |
|||
Last Updated: {{ $root.datetime(incident.lastUpdatedDate) }} ({{ dateFromNow(incident.lastUpdatedDate) }}) |
|||
</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; |
|||
}, |
|||
|
|||
}, |
|||
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; |
|||
}); |
|||
}, |
|||
|
|||
dateFromNow(date) { |
|||
return dayjs.utc(date).fromNow(); |
|||
}, |
|||
|
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../assets/vars.scss"; |
|||
|
|||
.overall-status { |
|||
font-weight: bold; |
|||
font-size: 25px; |
|||
|
|||
.ok { |
|||
color: $primary; |
|||
} |
|||
|
|||
.warning { |
|||
color: $warning; |
|||
} |
|||
|
|||
.danger { |
|||
color: $danger; |
|||
} |
|||
} |
|||
|
|||
h1 { |
|||
font-size: 30px; |
|||
|
|||
img { |
|||
vertical-align: middle; |
|||
height: 60px; |
|||
width: 60px; |
|||
} |
|||
} |
|||
|
|||
footer { |
|||
text-align: center; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.description span { |
|||
min-width: 50px; |
|||
} |
|||
|
|||
.logo-wrapper { |
|||
display: inline-block; |
|||
position: relative; |
|||
|
|||
&:hover { |
|||
.icon-upload { |
|||
transform: scale(1.2); |
|||
} |
|||
} |
|||
|
|||
.icon-upload { |
|||
transition: all $easing-in 0.2s; |
|||
position: absolute; |
|||
bottom: 6px; |
|||
font-size: 20px; |
|||
left: -14px; |
|||
background-color: white; |
|||
padding: 5px; |
|||
border-radius: 10px; |
|||
cursor: pointer; |
|||
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.9); |
|||
} |
|||
} |
|||
|
|||
.logo { |
|||
transition: all $easing-in 0.2s; |
|||
|
|||
&.edit-mode { |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
transform: scale(1.2); |
|||
} |
|||
} |
|||
} |
|||
|
|||
.incident { |
|||
.content { |
|||
&[contenteditable=true] { |
|||
min-height: 60px; |
|||
} |
|||
} |
|||
|
|||
.date { |
|||
font-size: 12px; |
|||
} |
|||
} |
|||
|
|||
.mobile { |
|||
h1 { |
|||
font-size: 22px; |
|||
} |
|||
|
|||
.overall-status { |
|||
font-size: 20px; |
|||
} |
|||
} |
|||
|
|||
</style> |
@ -1,70 +1,104 @@ |
|||
"use strict"; |
|||
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; |
|||
|
Loading…
Reference in new issue