diff --git a/assets/js/graph.js b/assets/js/graph.js index 5449aa0..72a99ac 100644 --- a/assets/js/graph.js +++ b/assets/js/graph.js @@ -149,7 +149,7 @@ export class GraphDisplayManager { let playerCount - if (id >= serverGraphData.length) { + if (id >= serverGraphData.length || typeof serverGraphData[id] !== 'number') { playerCount = '-' } else { playerCount = formatNumber(serverGraphData[id]) diff --git a/assets/js/scale.js b/assets/js/scale.js index 4a727d6..4b877e1 100644 --- a/assets/js/scale.js +++ b/assets/js/scale.js @@ -38,6 +38,10 @@ export class RelativeScale { } } + if (max === Number.MAX_VALUE) { + max = 0 + } + return RelativeScale.scale([0, max], tickCount) } @@ -57,14 +61,23 @@ export class RelativeScale { let max = Number.MIN_VALUE for (const point of data) { - if (point > max) { - max = point - } - if (point < min) { - min = point + if (typeof point === 'number') { + if (point > max) { + max = point + } + if (point < min) { + min = point + } } } + if (min === Number.MAX_VALUE) { + min = 0 + } + if (max === Number.MIN_VALUE) { + max = 0 + } + return [min, max] } } diff --git a/assets/js/servers.js b/assets/js/servers.js index fba696a..68113f7 100644 --- a/assets/js/servers.js +++ b/assets/js/servers.js @@ -63,7 +63,7 @@ export class ServerRegistration { this._app = app this.serverId = serverId this.data = data - this._graphData = [] + this._graphData = [[], []] this._failedSequentialPings = 0 } @@ -82,7 +82,16 @@ export class ServerRegistration { plugins: [ uPlotTooltipPlugin((pos, id, plot) => { if (pos) { - const text = formatNumber(plot.data[1][id]) + ' Players
' + formatTimestamp(plot.data[0][id] * 1000) + const playerCount = plot.data[1][id] + + if (typeof playerCount !== 'number') { + this._app.tooltip.hide() + + return + } + + // FIXME: update timestamp schema + const text = formatNumber(playerCount) + ' Players
' + formatTimestamp(plot.data[0][id] * 1000) this._app.tooltip.set(pos.left, pos.top, 10, 10, text) } else { @@ -147,23 +156,9 @@ export class ServerRegistration { } handlePing (payload, timestamp) { - if (typeof payload.playerCount !== 'undefined') { + if (typeof payload.playerCount === 'number') { this.playerCount = payload.playerCount - // Only update graph for successful pings - // This intentionally pauses the server graph when pings begin to fail - this._graphData[0].push(Math.floor(timestamp / 1000)) - this._graphData[1].push(this.playerCount) - - // Trim graphData to within the max length by shifting out the leading elements - for (const series of this._graphData) { - if (series.length > this._app.publicConfig.serverGraphMaxLength) { - series.shift() - } - } - - this.redraw() - // Reset failed ping counter to ensure the next connection error // doesn't instantly retrigger a layout change this._failedSequentialPings = 0 @@ -174,6 +169,34 @@ export class ServerRegistration { this.playerCount = 0 } } + + // Use payload.playerCount so nulls WILL be pushed into the graphing data + this._graphData[0].push(Math.floor(timestamp / 1000)) + this._graphData[1].push(payload.playerCount) + + // Trim graphData to within the max length by shifting out the leading elements + for (const series of this._graphData) { + if (series.length > this._app.publicConfig.serverGraphMaxLength) { + series.shift() + } + } + + this.redraw() + + if (typeof payload.playerCount !== 'undefined') { + this.playerCount = payload.playerCount || 0 + + // Use payload.playerCount so nulls WILL be pushed into the graphing data + this._graphData[0].push(Math.floor(timestamp / 1000)) + this._graphData[1].push(payload.playerCount) + + // Trim graphData to within the max length by shifting out the leading elements + for (const series of this._graphData) { + if (series.length > this._app.publicConfig.serverGraphMaxLength) { + series.shift() + } + } + } } redraw () { @@ -237,13 +260,23 @@ export class ServerRegistration { const playerCountLabelElement = document.getElementById('player-count_' + this.serverId) const errorElement = document.getElementById('error_' + this.serverId) - if (ping.error) { + if (ping.error || typeof ping.playerCount !== 'number') { // Hide any visible player-count and show the error element playerCountLabelElement.style.display = 'none' errorElement.style.display = 'block' - errorElement.innerText = ping.error.message - } else if (typeof ping.playerCount !== 'undefined') { + let errorMessage + + if (ping.error) { + errorMessage = ping.error.message + } else if (typeof ping.playerCount !== 'number') { + // If the frontend has freshly connection, and the server's last ping was in error, it may not contain an error object + // In this case playerCount will safely be null, so provide a generic error message instead + errorMessage = 'Failed to ping' + } + + errorElement.innerText = errorMessage + } else if (typeof ping.playerCount === 'number') { // Ensure the player-count element is visible and hide the error element playerCountLabelElement.style.display = 'block' errorElement.style.display = 'none' diff --git a/lib/ping.js b/lib/ping.js index 82d7e33..58ef9c8 100644 --- a/lib/ping.js +++ b/lib/ping.js @@ -92,8 +92,9 @@ class PingController { const result = results[serverRegistration.serverId] // Log to database if enabled + // Use null to represent a failed ping if (config.logToDatabase) { - const playerCount = result.resp ? result.resp.players.online : 0 + const playerCount = result.resp ? result.resp.players.online : null this._app.database.insertPing(serverRegistration.data.ip, timestamp, playerCount) } @@ -130,6 +131,12 @@ class PingController { const version = serverRegistration.getNextProtocolVersion() ping(serverRegistration, config.rates.connectTimeout, (err, resp) => { + if (Math.random() < 0.1) { + err = { + message: 'random fail' + } + resp = undefined + } if (err) { logger.log('error', 'Failed to ping %s: %s', serverRegistration.data.ip, err.message) } diff --git a/lib/servers.js b/lib/servers.js index 4f06507..a0ee646 100644 --- a/lib/servers.js +++ b/lib/servers.js @@ -21,7 +21,8 @@ class ServerRegistration { } handlePing (timestamp, resp, err, version) { - const playerCount = resp ? resp.players.online : 0 + // Use null to represent a failed ping + const playerCount = resp ? resp.players.online : null // Store into in-memory ping data this._pingHistory.push(playerCount) @@ -37,7 +38,7 @@ class ServerRegistration { let updateHistoryGraph = false if (config.logToDatabase) { - if (this.addGraphPoint(resp !== undefined, playerCount, timestamp)) { + if (this.addGraphPoint(playerCount, timestamp)) { updateHistoryGraph = true } } @@ -142,37 +143,21 @@ class ServerRegistration { for (const point of points) { // 0 is the index of the timestamp if (point[0] - lastTimestamp >= 60 * 1000) { - // This check tries to smooth out randomly dropped pings - // By default only filter pings that are online (playerCount > 0) - // This will keep looking forward until it finds a ping that is online - // If it can't find one within a reasonable timeframe, it will select a failed ping - if (point[0] - lastTimestamp >= 120 * 1000 || point[1] > 0) { - minutePoints.push(point) - lastTimestamp = point[0] - } + lastTimestamp = point[0] + + // FIXME: update schema, remove timestamp + minutePoints.push(point) } } if (minutePoints.length > 0) { this.graphData = minutePoints - - // Select the last entry to use for lastGraphDataPush - this._lastGraphDataPush = minutePoints[minutePoints.length - 1][0] } } - addGraphPoint (isSuccess, playerCount, timestamp) { - // If the ping failed, then to avoid destroying the graph, ignore it - // However if it's been too long since the last successful ping, push it anyways - if (this._lastGraphDataPush) { - const timeSince = timestamp - this._lastGraphDataPush - if ((isSuccess && timeSince < 60 * 1000) || (!isSuccess && timeSince < 70 * 1000)) { - return false - } - } - + addGraphPoint (playerCount, timestamp) { + // FIXME: update schema, remove timestamp this.graphData.push([timestamp, playerCount]) - this._lastGraphDataPush = timestamp // Trim old graphPoints according to #getMaxGraphDataLength if (this.graphData.length > TimeTracker.getMaxGraphDataLength()) {