Initial commit

This commit is contained in:
Dorian Niemiec 2024-08-06 14:49:57 +02:00
commit 04a8bac338
46 changed files with 1018 additions and 0 deletions

6
README.txt Normal file
View file

@ -0,0 +1,6 @@
The "backend" folder contains files, that would be copied to /usr/lib/svrjs directory.
The "frontend" folder contains files, that would be copied to /var/www/svrjs directory.
The "sql" folder contains a "database.sql" file, that would be loaded into a database.
You may need to set "useWebRootServerSideScript" in SVR.JS config.json to "false" and set up HTTP authentication at every URL, expect "/collect.svr" (that includes query strings)
The server-side script require "mysql" and "gnuplot" npm packages. It also requires "gnuplot" command to be installed.

6
backend/dbconfig.json Normal file
View file

@ -0,0 +1,6 @@
{
"host" : "localhost",
"user" : "statistics",
"password" : "statistics",
"database" : "statistics"
}

370
backend/serverSideScript.js Normal file
View file

@ -0,0 +1,370 @@
disableEndElseCallbackExecute = true; //Without "var", else it will not work!!!
var mysql = require("mysql");
var gnuplot = require("gnuplot"); //There is an OS command injection vulnerability in the "gnuplot" npm package, but since the statistics display part of the application doesn't involve user input, the application isn't affected by it.
if (!customvar1 && !customvar2) {
try {
customvar1 = JSON.parse(fs.readFileSync(__dirname + "/../dbconfig.json"));
} catch (err) {
customvar2 = err;
}
}
if (customvar2) {
// customvar2 is a instance of Error
callServerError(500, customvar2);
return;
}
// customvar1 is a database configuration
var connection = mysql.createConnection(customvar1);
function plot(data) {
var dataToFeed = [];
Object.keys(data).sort().forEach(function (key) {
dataToFeed.push(dataToFeed.length + " \"" + key.replace(/"/g, "'") + "\" " + parseFloat(data[key]));
});
var gnuplotObject = gnuplot().set("terminal png size 800,480").set("tics font \"Poppins,12\"").set("xtics rotate by 45 right").set("boxwidth 0.6").set("style fill solid").set("yrange [0:*]").set("grid ytics mytics").set("grid").plot("'-' using 1:3:xtic(2) notitle lc rgb \"#007000\" with boxes");
gnuplotObject.end(dataToFeed.join("\n"));
return gnuplotObject;
}
function antiXSS(string) {
return string.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
}
function getCount(period, callback) {
var query = "";
if (period == "daily") {
query = "SELECT COUNT(*) AS 'count' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY);";
} else if (period == "weekly") {
query = "SELECT COUNT(*) AS 'count' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 WEEK);";
} else if (period == "monthly") {
query = "SELECT COUNT(*) AS 'count' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH);";
} else if (period == "yearly" || period == "annual") {
query = "SELECT COUNT(*) AS 'count' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 YEAR);";
} else if (period == "total" || period == "all") {
query = "SELECT COUNT(*) AS 'count' FROM entries;";
} else {
callback(true, -1);
return;
}
connection.query(query, function (error, results, fields) {
if (error) {
callServerError(500, error);
if (connection.end) connection.end();
return;
}
callback(false, parseInt(results[0].count));
});
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
function formatTemplate(templateName, templateData, callback) {
readTemplate(templateName, function (data) {
callback(formatTemplateFromReadData(data, templateData));
});
}
function readTemplate(templateName, callback) {
fs.readFile(__dirname + "/../templates/" + templateName + ".template", function (err, data) {
if (err) {
callServerError(500, err);
if (connection.end) connection.end();
return;
}
callback(data);
});
}
function formatTemplateFromReadData(templateFileData, templateData) {
var tD = templateFileData.toString();
Object.keys(templateData).forEach(function (key) {
tD = tD.replace(new RegExp("\\{\\{" + escapeRegExp(key) + "\\}\\}", "g"), templateData[key]);
});
return tD;
}
if (href == "/") {
connection.connect(function (err) {
if (err) {
callServerError(500, err);
if (connection.end) connection.end();
return;
}
connection.query("SELECT id, ip, time, version, runtime, runtime_version FROM entries ORDER BY id DESC LIMIT 10;", function (error, results, fields) {
if (error) {
callServerError(500, err);
if (connection.end) connection.end();
return;
}
function getMods(callback, _id) {
if (!_id) _id = 0;
if (_id == results.length) {
callback();
return;
}
connection.query("SELECT name, version FROM entries_mods WHERE entry_id = " + mysql.escape(results[_id].id) + ";", function (error, mResults, mFields) {
if (error) {
callServerError(500, err);
if (connection.end) connection.end();
return;
}
results[_id].mods = mResults;
getMods(callback, _id + 1);
});
}
getMods(function () {
readTemplate("mods", function (modsData) {
readTemplate("mod", function (modData) {
readTemplate("lateststart", function (latestStartData) {
var latestStarts = "";
results.forEach(function (row) {
mods = "";
if (row.mods.length > 0) {
row.mods.forEach(function (mod) {
mods += formatTemplateFromReadData(modData, {
name: mod.name,
version: mod.version
});
});
mods = formatTemplateFromReadData(modsData, {
mods: mods
});
}
latestStarts += formatTemplateFromReadData(latestStartData, {
ipAddress: antiXSS(row.ip),
version: antiXSS(row.version),
runtime: antiXSS(row.runtime),
runtimeVersion: antiXSS(row.runtime_version),
mods: mods
});
});
getCount("daily", function (isBadRequest, dailyStarts) {
getCount("monthly", function (isBadRequest, monthlyStarts) {
getCount("yearly", function (isBadRequest, yearlyStarts) {
getCount("all", function (isBadRequest, totalStarts) {
formatTemplate("index", {
dailyStarts: dailyStarts,
monthlyStarts: monthlyStarts,
yearlyStarts: yearlyStarts,
totalStarts: totalStarts,
latestStarts: latestStarts
}, function (data) {
res.writeHead(200, {
"Content-Type": "text/html",
"Refresh": "60"
});
res.end(data);
if (connection.end) connection.end();
});
});
});
});
});
});
});
});
});
});
});
} else if (href == "/chart.svr") {
connection.connect(function (err) {
if (err) {
callServerError(500, err);
if (connection.end) connection.end();
return;
}
var query = "";
if (uobject.query.scope == "versiondistribution") {
query = "SELECT CONCAT('SVR.JS ', version) AS 'key', COUNT(*) AS 'value' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH) GROUP BY version;";
} else if (uobject.query.scope == "modsdistribution") {
query = "SELECT entries_mods.name AS 'key', COUNT(*) AS 'value' FROM entries_mods INNER JOIN entries ON entries_mods.entry_id = entries.id WHERE entries.time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH) GROUP BY entries_mods.name;";
} else if (uobject.query.scope == "jsruntimedistribution") {
query = "SELECT runtime AS 'key', COUNT(*) AS 'value' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH) GROUP BY runtime;";
} else if (uobject.query.scope == "jsruntimeversiondistribution") {
query = "SELECT CONCAT(runtime, ' ', runtime_version) AS 'key', COUNT(*) AS 'value' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH) GROUP BY runtime, runtime_version;";
} else {
callServerError(400);
if (connection.end) connection.end();
return;
}
connection.query(query, function (error, results, fields) {
if (error) {
callServerError(500, error);
if (connection.end) connection.end();
return;
}
var data = {};
results.forEach(function (keyValuePair) {
data[keyValuePair.key] = keyValuePair.value;
});
try {
var plotInstance = plot(data);
res.writeHead(200, {
"Content-Type": "image/png"
});
plotInstance.pipe(res);
if (connection.end) connection.end();
} catch (err) {
callServerError(500, err);
if (connection.end) connection.end();
return;
}
});
});
} else if (href == "/count.svr") {
connection.connect(function (err) {
if (err) {
callServerError(500, err);
if (connection.end) connection.end();
return;
}
getCount(uobject.query.period, function (isBadRequest, count) {
if (isBadRequest) {
callServerError(400);
if (connection.end) connection.end();
} else {
res.writeHead(200, {
"Content-Type": "text/plain"
});
res.end(count.toString());
if (connection.end) connection.end();
}
});
});
} else if (href == "/collect.svr") {
var headers = {
"Content-Type": "application/json"
};
if (req.method == "POST") {
if (req.headers["content-type"] && !req.headers["content-type"].match(/^application\/json(?:$|;)/)) {
headers["Accept"] = "application/json";
res.writeHead(415, headers);
res.end(JSON.stringify({
"status": 415,
"message": "Only JSON is supported."
}));
return;
}
var jsonData = "";
req.on("data", function (chunk) {
jsonData += chunk.toString();
});
req.on("end", function () {
try {
var parsedJsonData = {};
try {
parsedJsonData = JSON.parse(jsonData);
} catch (err) {
res.writeHead(400, headers);
res.end(JSON.stringify({
"status": 400,
"message": "JSON parse error."
}));
return;
}
var isValid = true;
if (!parsedJsonData.version || !parsedJsonData.runtime || !parsedJsonData.runtimeVersion || !parsedJsonData.mods || typeof parsedJsonData.version != "string" || typeof parsedJsonData.runtime != "string" || typeof parsedJsonData.runtimeVersion != "string" || !Array.isArray(parsedJsonData.mods)) {
isValid = false;
} else if (["Node.js", "Bun"].indexOf(parsedJsonData.runtime) == -1) {
isValid = false;
} else {
parsedJsonData.mods.every(function (element) {
if (!element.version || typeof element.name != "string" || typeof element.version != "string") {
isValid = false;
return false;
} else {
return true;
}
});
}
if (!isValid) {
res.writeHead(400, headers);
res.end(JSON.stringify({
"status": 400,
"message": "Invalid data."
}));
return;
}
connection.connect(function (err) {
if (err) {
serverconsole.errmessage("There was an error while processing the request!");
serverconsole.errmessage("Stack:");
serverconsole.errmessage(err.stack);
res.writeHead(500, headers);
res.end(JSON.stringify({
"status": 500,
"message": "An unexpected error occurred."
}));
if (connection.end) connection.end();
return;
}
var requestIP = (req.socket.realRemoteAddress ? req.socket.realRemoteAddress : req.socket.remoteAddress).replace(/^::ffff:/i, "");
connection.query("INSERT INTO entries (ip, time, version, runtime, runtime_version) VALUES (" + mysql.escape(requestIP) + ", NOW(), " + mysql.escape(parsedJsonData.version) + ", " + mysql.escape(parsedJsonData.runtime) + ", " + mysql.escape(parsedJsonData.runtimeVersion) + ");", function (error, results, fields) {
if (error) {
serverconsole.errmessage("There was an error while processing the request!");
serverconsole.errmessage("Stack:");
serverconsole.errmessage(error.stack);
res.writeHead(500, headers);
res.end(JSON.stringify({
"status": 500,
"message": "An unexpected error occurred."
}));
if (connection.end) connection.end();
return;
}
var entriesToInsert = [];
parsedJsonData.mods.forEach(function (mod) {
entriesToInsert.push("(" + mysql.escape(results.insertId) + ", " + mysql.escape(mod.name) + ", " + mysql.escape(mod.version) + ")");
});
connection.query("INSERT INTO entries_mods (entry_id, name, version) VALUES " + entriesToInsert.join(", ") + ";", function (error, results, fields) {
if (error) {
serverconsole.errmessage("There was an error while processing the request!");
serverconsole.errmessage("Stack:");
serverconsole.errmessage(error.stack);
res.writeHead(500, headers);
res.end(JSON.stringify({
"status": 500,
"message": "An unexpected error occurred."
}));
if (connection.end) connection.end();
return;
}
res.writeHead(200, headers);
res.end(JSON.stringify({
"status": 200,
"message": "The statistics are added successfully."
}));
if (connection.end) connection.end();
});
});
});
} catch (err) {
serverconsole.errmessage("There was an error while processing the request!");
serverconsole.errmessage("Stack:");
serverconsole.errmessage(err.stack);
res.writeHead(500, headers);
res.end(JSON.stringify({
"status": 500,
"message": "An unexpected error occurred."
}));
}
});
} else {
headers["Allow"] = "POST";
res.writeHead(405, headers);
res.end(JSON.stringify({
"status": 405,
"message": "Invalid HTTP method."
}));
}
} else {
elseCallback();
}

View file

@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/main.css">
<title>SVR.JS statistics</title>
</head>
<body>
<main>
<h1>SVR.JS statistics</h1>
<p>The page will be refreshed every minute.</p>
<h2>Insights</h2>
<ul class="insights">
<li>
<div class="insightwrapper">
<span class="insightheading">Daily SVR.JS starts</span>
<span class="insightvalue">{{dailyStarts}}</span>
</div>
</li>
<li>
<div class="insightwrapper">
<span class="insightheading">Monthly SVR.JS starts</span>
<span class="insightvalue">{{monthlyStarts}}</span>
</div>
</li>
<li>
<div class="insightwrapper">
<span class="insightheading">Yearly SVR.JS starts</span>
<span class="insightvalue">{{yearlyStarts}}</span>
</div>
</li>
<li>
<div class="insightwrapper">
<span class="insightheading">Total SVR.JS starts</span>
<span class="insightvalue">{{totalStarts}}</span>
</div>
</li>
<li>
<div class="insightwrapper">
<span class="insightheading">SVR.JS version distribution (monthly)</span>
<img src="/chart.svr?scope=versiondistribution" alt="SVR.JS version distribution" class="insightimage">
</div>
</li>
<li>
<div class="insightwrapper">
<span class="insightheading">SVR.JS mods distribution (monthly)</span>
<img src="/chart.svr?scope=modsdistribution" alt="SVR.JS mods distribution" class="insightimage">
</div>
</li>
<li>
<div class="insightwrapper">
<span class="insightheading">JavaScript runtime distribution (monthly)</span>
<img src="/chart.svr?scope=jsruntimedistribution" alt="JavaScript runtime distribution" class="insightimage">
</div>
</li>
<li>
<div class="insightwrapper">
<span class="insightheading">JS runtime version distribution (monthly)</span>
<img src="/chart.svr?scope=jsruntimeversiondistribution" alt="JavaScript runtime version distribution" class="insightimage">
</div>
</li>
</ul>
<div class="clearfix"></div>
<h2>10 latest SVR.JS starts</h2>
<ol class="latest">
{{latestStarts}}
</ol>
</main>
</body>
</html>

View file

@ -0,0 +1,5 @@
<li>
<span class="ipaddress">{{ipAddress}}</span><br/>
<span><b>SVR.JS version:</b> {{version}} | <b>JavaScript runtime:</b> {{runtime}} | <b>JavaScript runtime version:</b> {{runtimeVersion}}</span><br/>
{{mods}}
</li>

View file

@ -0,0 +1 @@
<li><b>{{name}}</b> {{version}}</li>

View file

@ -0,0 +1,4 @@
<span class="mods">Installed mods:</span>
<ul class="modlist">
{{mods}}
</ul>

492
frontend/css/main.css Normal file
View file

@ -0,0 +1,492 @@
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 100;
font-display: swap;
src: url(../fonts/pxiAyp8kv8JHgFVrJJLmE0tMMPKzSQ.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 100;
font-display: swap;
src: url(../fonts/pxiAyp8kv8JHgFVrJJLmE0tCMPI.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 200;
font-display: swap;
src: url(../fonts/pxiDyp8kv8JHgFVrJJLmv1pVGdeOcEg.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 200;
font-display: swap;
src: url(../fonts/pxiDyp8kv8JHgFVrJJLmv1pVF9eO.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url(../fonts/pxiDyp8kv8JHgFVrJJLm21lVGdeOcEg.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url(../fonts/pxiDyp8kv8JHgFVrJJLm21lVF9eO.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url(../fonts/pxiGyp8kv8JHgFVrJJLufntAKPY.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url(../fonts/pxiGyp8kv8JHgFVrJJLucHtA.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url(../fonts/pxiDyp8kv8JHgFVrJJLmg1hVGdeOcEg.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url(../fonts/pxiDyp8kv8JHgFVrJJLmg1hVF9eO.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url(../fonts/pxiDyp8kv8JHgFVrJJLmr19VGdeOcEg.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url(../fonts/pxiDyp8kv8JHgFVrJJLmr19VF9eO.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url(../fonts/pxiDyp8kv8JHgFVrJJLmy15VGdeOcEg.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url(../fonts/pxiDyp8kv8JHgFVrJJLmy15VF9eO.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url(../fonts/pxiDyp8kv8JHgFVrJJLm111VGdeOcEg.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url(../fonts/pxiDyp8kv8JHgFVrJJLm111VF9eO.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 900;
font-display: swap;
src: url(../fonts/pxiDyp8kv8JHgFVrJJLm81xVGdeOcEg.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 900;
font-display: swap;
src: url(../fonts/pxiDyp8kv8JHgFVrJJLm81xVF9eO.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 100;
font-display: swap;
src: url(../fonts/pxiGyp8kv8JHgFVrLPTufntAKPY.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 100;
font-display: swap;
src: url(../fonts/pxiGyp8kv8JHgFVrLPTucHtA.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 200;
font-display: swap;
src: url(../fonts/pxiByp8kv8JHgFVrLFj_Z1JlFc-K.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 200;
font-display: swap;
src: url(../fonts/pxiByp8kv8JHgFVrLFj_Z1xlFQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(../fonts/pxiByp8kv8JHgFVrLDz8Z1JlFc-K.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(../fonts/pxiByp8kv8JHgFVrLDz8Z1xlFQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../fonts/pxiEyp8kv8JHgFVrJJnecmNE.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../fonts/pxiEyp8kv8JHgFVrJJfecg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(../fonts/pxiByp8kv8JHgFVrLGT9Z1JlFc-K.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(../fonts/pxiByp8kv8JHgFVrLGT9Z1xlFQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(../fonts/pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(../fonts/pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../fonts/pxiByp8kv8JHgFVrLCz7Z1JlFc-K.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../fonts/pxiByp8kv8JHgFVrLCz7Z1xlFQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(../fonts/pxiByp8kv8JHgFVrLDD4Z1JlFc-K.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(../fonts/pxiByp8kv8JHgFVrLDD4Z1xlFQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(../fonts/pxiByp8kv8JHgFVrLBT5Z1JlFc-K.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(../fonts/pxiByp8kv8JHgFVrLBT5Z1xlFQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
body {
font-family: Poppins, sans-serif;
font-size: 1.25em;
background-color: #ffffff;
color: #000000;
}
main {
width: 100%;
max-width: 1024px;
margin: 3em auto;
overflow-wrap: break-word;
}
h1 {
font-size: 3em;
margin-top: 0.3em;
margin-bottom: 0.3em;
}
h2 {
margin-top: 0.75em;
margin-bottom: 0.75em;
}
.latest {
list-style-type: none;
padding: 0;
}
.latest li {
margin: 0.6em;
padding: 0.6em;
font-size: 0.8em;
background-color: #f9f9fa;
border: 1px solid #e4e4e7;
border-radius: 10px;
}
.ipaddress {
font-size: 1.75em;
font-weight: bold;
}
.mods {
font-size: 1.25em;
font-weight: bold;
}
.modlist {
border: 1px solid #e4e4e7;
border-radius: 10px;
padding: 0;
list-style-type: none;
background-color: #ffffff;
}
.modlist li {
border-radius: 0px;
border: none;
border-bottom: 1px solid #e4e4e7;
background: none;
font-size: 1em;
margin: 0;
padding: 0.4em;
}
.modlist li:last-child {
border-bottom: none;
}
.insights {
list-style-type: none;
padding: 0;
}
.insights:after {
clear: both;
display: table;
content: '';
}
.insights li {
width: 50%;
float: left;
}
.insightwrapper {
margin: 0.6em;
padding: 0.6em;
font-size: 0.8em;
background-color: #f9f9fa;
border: 1px solid #e4e4e7;
border-radius: 10px;
}
.insightheading {
display: block;
font-weight: bold;
font-size: 1.25em;
}
.insightvalue {
font-size: 3em;
}
.insightimage {
width: 100%;
border-radius: 15px;
}
.clearfix {
display: table;
clear: both;
}
@media screen and (prefers-color-scheme: dark) {
body {
background-color: #0c0a09;
color: #ffffff;
}
a {
color: #ffffff;
}
.latest li {
border-color: #27272a;
background-color: #191817;
}
.modlist {
border-color: #27272a;
background-color: #0c0a09;
}
.modlist li {
border-color: #27272a;
background: none;
}
.insightwrapper {
border-color: #27272a;
background-color: #191817;
}
}
@media screen and (max-width: 800px) {
body {
font-size: 1.1em;
}
}
@media screen and (max-width: 600px) {
body {
font-size: 1em;
}
main {
margin: 2em auto;
}
.insights li {
width: 100%;
float: none;
}
}

BIN
frontend/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

63
sql/database.sql Normal file
View file

@ -0,0 +1,63 @@
/*!999999\- enable the sandbox mode */
-- MariaDB dump 10.19 Distrib 10.6.18-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: statistics
-- ------------------------------------------------------
-- Server version 10.6.18-MariaDB-0ubuntu0.22.04.1
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `entries`
--
DROP TABLE IF EXISTS `entries`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `entries` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`ip` varchar(100) NOT NULL,
`time` datetime NOT NULL,
`version` varchar(100) NOT NULL,
`runtime` varchar(100) NOT NULL,
`runtime_version` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `entries_mods`
--
DROP TABLE IF EXISTS `entries_mods`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `entries_mods` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`entry_id` int(10) NOT NULL,
`name` varchar(100) NOT NULL,
`version` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2024-08-06 14:32:28