Minetrack/app.js
Nick Krecklow f875361bc7
Minetrack 5 (#143)
* remove unused #getServer methods, inline #roundToPoint

* replace #safeName regex with incremental ids

* remove legacy #setInterval based #updateMojangServices handling

* add Tooltip class, move faviconSize to css instead of js

* move server id assignment to ServerRegistry

* move printPort logic to formatMinecraftServerAddress, add MINECRAFT_DEFAULT_PORTS

* simplify ping tracking

* rework perc-bar tooltip to not use mousemove event

* begin moving graphing logic to GraphDisplayManager

* begin merge graph point tracking into graphDisplayManager

* centralizing graphing logic into GraphDisplayManager

* properly reset GraphDisplayManager when handling disconnects

* move individual server graph data into ServerGraph class

* constantly run sortServers loop to simplify logic

* inline #updateMojangServices method

* resize performance improvements

* remove legacy bootTime refresh behavior, require manual user refresh

* move class defs to core.js

* remove unused #isGraphDataVisible arg

* remove #toggleControlsDrawer

* dont call #updatePercentageBar in #updateServerStatus calls

* centralize caption handling

* inline #msToTime

* remove hackish seconds handling for timestamps

* reduce #forEach calls with filter/map

* safely fallback to errorMessage if errno/description does not match

* Add /images/missing_favicon.png path instead of putting base64 in js

* remove debug

* cleanup mojang status handling

* move historyPlot instance into GraphDisplayManager

* cleanup checkbox html generation

* cleanup #updateServerStatus

* fix up tooltip styling

* move jquery code out of core.js

* fix add server race condition when initially pinging servers

* send error.placeholder=true for pending pings so the frontend can discard later

* filter placeholder pings sent by the backend

* del assets/images/logo_2014.png

* move graph code into graph.js

* merge pingTracker into ServerRegistry+ServerGraph

* remove todos

* simplify getVisibleGraphData

* fix potential sortServers race condition when adding

* use #show instead of #fadeIn(0)

* remove publicConfig.json, send over socket

* update docs/CHANGELOG.md

* getOrAssign -> getOrCreateId

* dont delete graph controls when disconnected

* early work cleaning up HTML+CSS structures

* cleanup server css elements

* cleanup graph control css elements

* move base CSS color values into @media(prefers-color-scheme: light)

* move CSS magic colors to vars

* reduce duplicated CSS color rules

* inline body text color CSS

* WIP replacing jQuery calls with vanilla JS

* WIP replacing jQuery calls with vanilla JS

* replace getElementsByClass with querySelectorAll

* typeMarker -> serverTypeHTML

* use jQuery slim for remaining flot.js dependency

* merge setAllGraphVisibility into GraphDisplayManager

* break apart element update and redraw logic

* add eslint + parcel bundler

* auto lint assets/js when building

* statically serve favicons/ for faviconOverrides outside of dist/

* only send favicons when changed

* move faviconOverride behavior into entry in servers.json

* add warning to backend server files

* remove .server-favicon-missing class

* add Minetrack 5 migration guide

* add npm run build step to install.sh

* adjust package.json version to 5.0.0

* remove js references from index.html

* move logic and behavior out of site.js

* cleanup ServerRegistry methods

* prevent multiple history graph redraws

* add comments

* cleanup #addServer usage, move to App

* move graph control bindings into GraphDisplayManager

* site.js -> main.js, core.js -> servers.js

* move Tooltip/Caption into util.js

* spacing tweak

* format index.html

* ensure the frontend does not handling updateHistoryGraph events

* prevent versions/record updates if the same value

* avoid empty percbar updates, ensure versions are sorted

* only include main.js ref in index.html

* serve minified copy of font awesome directly

* bundle icons.css into main.css, remove Open Sans 400

* add new SVG logo

* update docs/CHANGELOG.md

* new design, server version grouping

* remove start.sh call from install.sh

* move graph controls into header with new button

* move #handleSettingsToggle back to graph

* fix legacy code behavior of currentVersionIndex applying globally

* fix header text color in light mode

* fix mojang status text color in light mode

* fix toggle settings and checkbox colors

* tweak button hover color

* tweak button hover color

* add new status-overlay to avoid complicated DOM management during loading

* fix initial graph rendering bug

* add comments

* update default graph tick sizes

* prevent #tooltip from overflowing page

* remove localhost spec

* prevent minor connection errors from reshuffling layout

* update CHANGELOG.md

* add message/button for manually loading historical graph on mobile devices

* send isGraphVisible to frontend to prevent alert if logToDatabase: false

* send timestamp data with record

* update docs/CHANGELOG.md

* remove clock icon

* remove 24h peak timestamp

* Only check favicon if present

* safely handle undefined/empty knownVersions in #formatMinecraftVersions

* merge config.versions and minecraft.json into minecraft_versions.json, simplify index matching behavior

* remove localhost url in socket.io config

* stub methods/linkage for FocusManager

* add #isObjectEqual hack, add event proxying to FocusManager

* wip extended stats box

* remove server-type badging

* tweak mojang unstable color

* serve socket.io-client using parcel

* fix incorrect mojang status colors

* remove legacy capitalization design

* redesign focus boxes

* update docs/CHANGELOG.md

* remove localhost ref

* color clock icon

* use background-color for hover effect, remove unused var

* improve stats focus box icons

* change mojang sessions icon to globe

* Add favorites system

* remove focus boxes

* update docs/CHANGELOG.md

* remove focus icons from font

* simplify graph related event binding

* Add Sort By button

* store current sortOption in localStorage

* update docs/CHANGELOG.md

* move magic 0 sortOption to SORT_OPTION_INDEX_DEFAULT

* remove localhost ref

* merge #settings-toggle, #sort-by and .mojang-status CSS

* remove .focus-box CSS

* use sortedServerIds for _lastSortedServers

* tweak --color-blue

* new missing_favicon design to match logo

* edit footer CSS/text, remove github icon

* replace player count diff counter with GROWTH sort option

* italize non-default sort options

* add Only Favorites button to auto sync favorites to the visible graph data

* add icons to graph control buttons

* update docs/CHANGELOG.md

* use * to denote non-default sort option instead

* remove localhost url in socket.io config

* add value highlighting to make sort by easier to read

* remove last remaining uppercase text

* remove serverTypesVisible from config.json

* simplify header CSS, fix spacing with logToDatabase=false

* fix inverted text color on highlighted values

* remove localhost url in socket.io config

* break header into rows on mobile devices

Co-authored-by: Hugo Manrique <contact@hugmanrique.me>
2020-04-19 19:27:59 -05:00

393 lines
12 KiB
JavaScript

/**
* THIS IS LEGACY, UNMAINTAINED CODE
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
* USAGE IS NOT RECOMMENDED
*/
var server = require('./lib/server');
var ping = require('./lib/ping');
var logger = require('./lib/logger');
var mojang = require('./lib/mojang_services');
var util = require('./lib/util');
var db = require('./lib/database');
var config = require('./config.json');
var servers = require('./servers.json');
var minecraftVersions = require('./minecraft_versions.json');
var networkHistory = [];
var connectedClients = 0;
var networkVersions = [];
const lastFavicons = [];
var graphData = [];
var highestPlayerCount = {};
var lastGraphPush = [];
var graphPeaks = {};
const serverProtocolVersionIndexes = []
function getNextProtocolVersion (server) {
// Minecraft Bedrock Edition does not have protocol versions
if (server.type === 'PE') {
return {
protocolId: 0,
protocolIndex: 0
}
}
const protocolVersions = minecraftVersions[server.type]
let nextProtocolVersion = serverProtocolVersionIndexes[server.name]
if (typeof nextProtocolVersion === 'undefined' || nextProtocolVersion + 1 >= protocolVersions.length) {
nextProtocolVersion = 0
} else {
nextProtocolVersion++
}
serverProtocolVersionIndexes[server.name] = nextProtocolVersion
return {
protocolId: protocolVersions[nextProtocolVersion].protocolId,
protocolIndex: nextProtocolVersion
}
}
function pingAll() {
for (var i = 0; i < servers.length; i++) {
// Make sure we lock our scope.
(function(network) {
// Asign auto generated color if not present
if (!network.color) {
network.color = util.stringToColor(network.name);
}
const attemptedVersion = getNextProtocolVersion(network)
ping.ping(network.ip, network.port, network.type, config.rates.connectTimeout, function(err, res) {
// Handle our ping results, if it succeeded.
if (err) {
logger.log('error', 'Failed to ping ' + network.ip + ': ' + err.message);
}
// If we have favicon override specified, use it.
if (network.favicon) {
res.favicon = network.favicon
}
handlePing(network, res, err, attemptedVersion);
}, attemptedVersion.protocolId);
})(servers[i]);
}
}
// This is where the result of a ping is feed.
// This stores it and converts it to ship to the frontend.
function handlePing(network, res, err, attemptedVersion) {
// Log our response.
if (!networkHistory[network.name]) {
networkHistory[network.name] = [];
}
// Update the version list
if (!networkVersions[network.name]) {
networkVersions[network.name] = [];
}
const serverVersionHistory = networkVersions[network.name]
// If the result version matches the attempted version, the version is supported
if (res && res.version !== undefined) {
const indexOf = serverVersionHistory.indexOf(attemptedVersion.protocolIndex)
// Test indexOf to avoid inserting previously recorded protocolIndex values
if (res.version === attemptedVersion.protocolId && indexOf === -1) {
serverVersionHistory.push(attemptedVersion.protocolIndex)
} else if (res.version !== attemptedVersion.protocolId && indexOf >= 0) {
serverVersionHistory.splice(indexOf, 1)
}
}
const timestamp = util.getCurrentTimeMs()
if (res) {
const recordData = highestPlayerCount[network.ip]
// Validate that we have logToDatabase enabled otherwise in memory pings
// will create a record that's only valid for the runtime duration.
if (config.logToDatabase && (!recordData || res.players.online > recordData.playerCount)) {
highestPlayerCount[network.ip] = {
playerCount: res.players.online,
timestamp: timestamp
}
}
}
// Update the clients
var networkSnapshot = {
info: {
name: network.name,
timestamp: timestamp,
type: network.type
},
versions: serverVersionHistory,
recordData: highestPlayerCount[network.ip]
};
if (res) {
networkSnapshot.result = res;
// Only emit updated favicons
// Favicons will otherwise be explicitly emitted during the handshake process
if (res.favicon) {
const lastFavicon = lastFavicons[network.name]
if (lastFavicon !== res.favicon) {
lastFavicons[network.name] = res.favicon
networkSnapshot.favicon = res.favicon // Send updated favicon directly on object
}
delete res.favicon // Never store favicons in memory outside lastFavicons
}
} else if (err) {
networkSnapshot.error = err;
}
server.io.sockets.emit('update', networkSnapshot);
var _networkHistory = networkHistory[network.name];
// Remove our previous data that we don't need anymore.
for (var i = 0; i < _networkHistory.length; i++) {
delete _networkHistory[i].versions
delete _networkHistory[i].info;
}
_networkHistory.push({
error: err,
result: res,
versions: serverVersionHistory,
timestamp: timestamp,
info: {
ip: network.ip,
port: network.port,
type: network.type,
name: network.name
}
});
// Make sure we never log too much.
if (_networkHistory.length > 72) { // 60/2.5 = 24, so 24 is one minute
_networkHistory.shift();
}
// Log it to the database if needed.
if (config.logToDatabase) {
db.log(network.ip, timestamp, res ? res.players.online : 0);
}
// The same mechanic from trimUselessPings is seen here.
// If we dropped the ping, then to avoid destroying the graph, ignore it.
// However if it's been too long since the last successful ping, we'll send it anyways.
if (config.logToDatabase) {
if (!lastGraphPush[network.ip] || (timestamp - lastGraphPush[network.ip] >= 60 * 1000 && res) || timestamp - lastGraphPush[network.ip] >= 70 * 1000) {
lastGraphPush[network.ip] = timestamp;
// Don't have too much data!
util.trimOldPings(graphData);
if (!graphData[network.name]) {
graphData[network.name] = [];
}
graphData[network.name].push([timestamp, res ? res.players.online : 0]);
// Send the update.
server.io.sockets.emit('updateHistoryGraph', {
ip: network.ip,
name: network.name,
players: (res ? res.players.online : 0),
timestamp: timestamp
});
}
// Update calculated graph peak regardless if the graph is being updated
// This can cause a (harmless) desync between live and stored data, but it allows it to be more accurate for long surviving processes
var networkData = graphData[network.name];
if (networkData) {
var graphPeakIndex = -1;
var graphPeakPlayerCount = 0;
for (var i = 0; i < networkData.length; i++) {
// [1] refers to the online player count
var point = networkData[i][1];
if (point > 0 && (graphPeakIndex === -1 || point > graphPeakPlayerCount)) {
graphPeakIndex = i;
graphPeakPlayerCount = point;
}
}
// Test if a highest index has been selected and has changed from any previous selections
var previousPeak = graphPeaks[network.name];
// [1] refers to the online player count
if (graphPeakIndex !== -1 && (!previousPeak || previousPeak[1] !== graphPeakPlayerCount)) {
var graphPeakData = networkData[graphPeakIndex];
graphPeaks[network.name] = graphPeakData;
// Broadcast update event to clients
server.io.sockets.emit('updatePeak', {
ip: network.ip,
name: network.name,
players: graphPeakData[1],
timestamp: graphPeakData[0]
});
}
}
}
}
// Start our main loop that does everything.
function startMainLoop() {
util.setIntervalNoDelay(pingAll, config.rates.pingAll);
util.setIntervalNoDelay(function() {
mojang.update(config.rates.mojangStatusTimeout);
server.io.sockets.emit('updateMojangServices', mojang.toMessage());
}, config.rates.upateMojangStatus);
}
function startServices() {
server.start();
// Track how many people are currently connected.
server.io.on('connect', function(client) {
// We're good to connect them!
connectedClients += 1;
logger.log('info', '%s connected, total clients: %d', util.getRemoteAddr(client.request), connectedClients);
// Attach our listeners.
client.on('disconnect', function() {
connectedClients -= 1;
logger.log('info', '%s disconnected, total clients: %d', util.getRemoteAddr(client.request), connectedClients);
});
client.on('requestHistoryGraph', function() {
if (config.logToDatabase) {
// Send them the big 24h graph.
client.emit('historyGraph', graphData);
// Send current peaks, if any
if (Object.keys(graphPeaks).length > 0) {
client.emit('peaks', graphPeaks);
}
}
});
const minecraftVersionNames = {}
Object.keys(minecraftVersions).forEach(function (key) {
minecraftVersionNames[key] = minecraftVersions[key].map(version => version.name)
})
// Send configuration data for rendering the page
client.emit('setPublicConfig', {
graphDuration: config.graphDuration,
servers: servers,
minecraftVersions: minecraftVersionNames,
isGraphVisible: config.logToDatabase
});
// Send them our previous data, so they have somewhere to start.
client.emit('updateMojangServices', mojang.toMessage());
// Send each individually, this should look cleaner than waiting for one big array to transfer.
for (var i = 0; i < servers.length; i++) {
var server = servers[i];
if (!(server.name in networkHistory) || networkHistory[server.name].length < 1) {
// This server hasn't been ping'd yet. Send a hacky placeholder.
client.emit('add', [[{
error: {
description: 'Waiting...',
placeholder: true
},
result: null,
timestamp: util.getCurrentTimeMs(),
info: {
ip: server.ip,
port: server.port,
type: server.type,
name: server.name
}
}]]);
} else {
// Append the lastFavicon to the last ping entry
const serverHistory = networkHistory[server.name];
const lastFavicon = lastFavicons[server.name];
if (lastFavicon) {
serverHistory[serverHistory.length - 1].favicon = lastFavicon
}
client.emit('add', [serverHistory])
}
}
client.emit('syncComplete');
});
startMainLoop();
}
logger.log('info', 'Booting, please wait...');
if (config.logToDatabase) {
// Setup our database.
db.setup();
var timestamp = util.getCurrentTimeMs();
db.queryPings(config.graphDuration, function(data) {
graphData = util.convertServerHistory(data);
completedQueries = 0;
logger.log('info', 'Queried and parsed ping history in %sms', util.getCurrentTimeMs() - timestamp);
for (var i = 0; i < servers.length; i++) {
// Compute graph peak from historical data
var networkData = graphData[servers[i].name];
if (networkData) {
var graphPeakIndex = -1;
var graphPeakPlayerCount = 0;
for (var x = 0; x < networkData.length; x++) {
// [1] refers to the online player count
var point = networkData[x][1];
if (point > 0 && (graphPeakIndex === -1 || point > graphPeakPlayerCount)) {
graphPeakIndex = x;
graphPeakPlayerCount = point;
}
}
if (graphPeakIndex !== -1) {
graphPeaks[servers[i].name] = networkData[graphPeakIndex];
logger.log('info', 'Selected graph peak %d (%s)', networkData[graphPeakIndex][1], servers[i].name);
}
}
(function(server) {
db.getTotalRecord(server.ip, function(playerCount, timestamp) {
logger.log('info', 'Computed total record %s (%d) @ %d', server.ip, playerCount, timestamp);
highestPlayerCount[server.ip] = {
playerCount: playerCount,
timestamp: timestamp
};
completedQueries += 1;
if (completedQueries === servers.length) {
startServices();
}
});
})(servers[i]);
}
});
} else {
logger.log('warn', 'Database logging is not enabled. You can enable it by setting "logToDatabase" to true in config.json. This requires sqlite3 to be installed.');
startServices();
}