diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 000000000..e8217e3a4 --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,41 @@ +name: autofix.ci + +on: + push: + branches: [ "master", "1.23.X"] + pull_request: +permissions: {} + +jobs: + autofix: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { persist-credentials: false } + + - name: Cache/Restore node_modules + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + id: node-modules-cache + with: + path: node_modules + key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + + - name: Setup Node.js + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Auto-fix JavaScript/Vue linting issues + run: npm run lint-fix:js + continue-on-error: true + + - name: Auto-fix CSS/SCSS linting issues + run: npm run lint-fix:style + continue-on-error: true + + - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 \ No newline at end of file diff --git a/db/knex_migrations/2025-02-17-2142-generalize-analytics.js b/db/knex_migrations/2025-02-17-2142-generalize-analytics.js new file mode 100644 index 000000000..8c5fda990 --- /dev/null +++ b/db/knex_migrations/2025-02-17-2142-generalize-analytics.js @@ -0,0 +1,23 @@ +// Udpate status_page table to generalize analytics fields +exports.up = function (knex) { + return knex.schema + .alterTable("status_page", function (table) { + table.renameColumn("google_analytics_tag_id", "analytics_id"); + table.string("analytics_script_url"); + table.enu("analytics_type", [ "google", "umami", "plausible", "matomo" ]).defaultTo(null); + + }).then(() => { + // After a succesful migration, add google as default for previous pages + knex("status_page").whereNotNull("analytics_id").update({ + "analytics_type": "google", + }); + }); +}; + +exports.down = function (knex) { + return knex.schema.alterTable("status_page", function (table) { + table.renameColumn("analytics_id", "google_analytics_tag_id"); + table.dropColumn("analytics_script_url"); + table.dropColumn("analytics_type"); + }); +}; diff --git a/db/knex_migrations/2026-01-02-0713-gamedig-v4-to-v5.js b/db/knex_migrations/2026-01-02-0713-gamedig-v4-to-v5.js new file mode 100644 index 000000000..63931a705 --- /dev/null +++ b/db/knex_migrations/2026-01-02-0713-gamedig-v4-to-v5.js @@ -0,0 +1,198 @@ +// Migration to update monitor.game from GameDig v4 to v5 game IDs +// Reference: https://github.com/gamedig/node-gamedig/blob/master/MIGRATE_IDS.md + +// Lookup table mapping v4 game IDs to v5 game IDs +const gameDig4to5IdMap = { + "americasarmypg": "aapg", + "7d2d": "sdtd", + "as": "actionsource", + "ageofchivalry": "aoc", + "arkse": "ase", + "arcasimracing": "asr08", + "arma": "aaa", + "arma2oa": "a2oa", + "armacwa": "acwa", + "armar": "armaresistance", + "armare": "armareforger", + "armagetron": "armagetronadvanced", + "bat1944": "battalion1944", + "bf1942": "battlefield1942", + "bfv": "battlefieldvietnam", + "bf2": "battlefield2", + "bf2142": "battlefield2142", + "bfbc2": "bbc2", + "bf3": "battlefield3", + "bf4": "battlefield4", + "bfh": "battlefieldhardline", + "bd": "basedefense", + "bs": "bladesymphony", + "buildandshoot": "bas", + "cod4": "cod4mw", + "callofjuarez": "coj", + "chivalry": "cmw", + "commandos3": "c3db", + "cacrenegade": "cacr", + "contactjack": "contractjack", + "cs15": "counterstrike15", + "cs16": "counterstrike16", + "cs2": "counterstrike2", + "crossracing": "crce", + "darkesthour": "dhe4445", + "daysofwar": "dow", + "deadlydozenpt": "ddpt", + "dh2005": "deerhunter2005", + "dinodday": "ddd", + "dirttrackracing2": "dtr2", + "dmc": "deathmatchclassic", + "dnl": "dal", + "drakan": "dootf", + "dys": "dystopia", + "em": "empiresmod", + "empyrion": "egs", + "f12002": "formulaone2002", + "flashpointresistance": "ofr", + "fivem": "gta5f", + "forrest": "theforrest", + "graw": "tcgraw", + "graw2": "tcgraw2", + "giantscitizenkabuto": "gck", + "ges": "goldeneyesource", + "gore": "gus", + "hldm": "hld", + "hldms": "hlds", + "hlopfor": "hlof", + "hl2dm": "hl2d", + "hidden": "thehidden", + "had2": "hiddendangerous2", + "igi2": "i2cs", + "il2": "il2sturmovik", + "insurgencymic": "imic", + "isle": "theisle", + "jamesbondnightfire": "jb007n", + "jc2mp": "jc2m", + "jc3mp": "jc3m", + "kingpin": "kloc", + "kisspc": "kpctnc", + "kspdmp": "kspd", + "kzmod": "kreedzclimbing", + "left4dead": "l4d", + "left4dead2": "l4d2", + "m2mp": "m2m", + "mohsh": "mohaas", + "mohbt": "mohaab", + "mohab": "moha", + "moh2010": "moh", + "mohwf": "mohw", + "minecraftbe": "mbe", + "mtavc": "gtavcmta", + "mtasa": "gtasamta", + "ns": "naturalselection", + "ns2": "naturalselection2", + "nwn": "neverwinternights", + "nwn2": "neverwinternights2", + "nolf": "tonolf", + "nolf2": "nolf2asihw", + "pvkii": "pvak2", + "ps": "postscriptum", + "primalcarnage": "pce", + "pc": "projectcars", + "pc2": "projectcars2", + "prbf2": "prb2", + "przomboid": "projectzomboid", + "quake1": "quake", + "quake3": "q3a", + "ragdollkungfu": "rdkf", + "r6": "rainbowsix", + "r6roguespear": "rs2rs", + "r6ravenshield": "rs3rs", + "redorchestraost": "roo4145", + "redm": "rdr2r", + "riseofnations": "ron", + "rs2": "rs2v", + "samp": "gtasam", + "saomp": "gtasao", + "savage2": "s2ats", + "ss": "serioussam", + "ss2": "serioussam2", + "ship": "theship", + "sinep": "sinepisodes", + "sonsoftheforest": "sotf", + "swbf": "swb", + "swbf2": "swb2", + "swjk": "swjkja", + "swjk2": "swjk2jo", + "takeonhelicopters": "toh", + "tf2": "teamfortress2", + "terraria": "terrariatshock", + "tribes1": "t1s", + "ut": "unrealtournament", + "ut2003": "unrealtournament2003", + "ut2004": "unrealtournament2004", + "ut3": "unrealtournament3", + "v8supercar": "v8sc", + "vcmp": "vcm", + "vs": "vampireslayer", + "wheeloftime": "wot", + "wolfenstein2009": "wolfenstein", + "wolfensteinet": "wet", + "wurm": "wurmunlimited", +}; + +/** + * Migrate game IDs from v4 to v5 + * @param {import("knex").Knex} knex - Knex instance + * @returns {Promise} + */ +exports.up = async function (knex) { + await knex.transaction(async (trx) => { + // Get all monitors that use the gamedig type + const monitors = await trx("monitor") + .select("id", "game") + .where("type", "gamedig") + .whereNotNull("game"); + + // Update each monitor with the new game ID if it needs migration + for (const monitor of monitors) { + const oldGameId = monitor.game; + const newGameId = gameDig4to5IdMap[oldGameId]; + + if (newGameId) { + await trx("monitor") + .where("id", monitor.id) + .update({ game: newGameId }); + } + } + }); +}; + +/** + * Revert game IDs from v5 back to v4 + * @param {import("knex").Knex} knex - Knex instance + * @returns {Promise} + */ +exports.down = async function (knex) { + // Create reverse mapping from the same LUT + const gameDig5to4IdMap = Object.fromEntries( + Object.entries(gameDig4to5IdMap).map(([ v4, v5 ]) => [ v5, v4 ]) + ); + + await knex.transaction(async (trx) => { + // Get all monitors that use the gamedig type + const monitors = await trx("monitor") + .select("id", "game") + .where("type", "gamedig") + .whereNotNull("game"); + + // Revert each monitor back to the old game ID if it was migrated + for (const monitor of monitors) { + const newGameId = monitor.game; + const oldGameId = gameDig5to4IdMap[newGameId]; + + if (oldGameId) { + await trx("monitor") + .where("id", monitor.id) + .update({ game: oldGameId }); + } + } + }); +}; diff --git a/package-lock.json b/package-lock.json index ab56e410d..12ace22aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "express-static-gzip": "~2.1.7", "feed": "^4.2.2", "form-data": "~4.0.0", - "gamedig": "^4.2.0", + "gamedig": "^5.0.1", "html-escaper": "^3.0.3", "http-cookie-agent": "~5.0.4", "http-graceful-shutdown": "~3.1.7", @@ -4956,12 +4956,12 @@ "license": "BSD-3-Clause" }, "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sindresorhus/is?sponsor=1" @@ -5760,18 +5760,6 @@ "@popperjs/core": "^2.9.2" } }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -5893,15 +5881,6 @@ "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", "license": "MIT" }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/koa": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", @@ -5984,15 +5963,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", @@ -7820,54 +7790,30 @@ } }, "node_modules/cacheable-lookup": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.1.0.tgz", - "integrity": "sha512-KJ/Dmo1lDDhmW2XDPMo+9oiy/CeqosPguPCrgcVzKyZrL6pM1gU2GmPY/xo6OQPTUaA/c0kwHuywB4E6nmT9ww==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", "license": "MIT", "engines": { - "node": ">=10.6.0" + "node": ">=14.16" } }, "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", "license": "MIT", "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cacheable-request/node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "license": "MIT", - "engines": { - "node": ">=8" + "node": ">=14.16" } }, "node_modules/call-bind": { @@ -8200,18 +8146,6 @@ "node": ">=12" } }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -9543,6 +9477,14 @@ "dev": true, "license": "ISC" }, + "node_modules/emitter-component": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.2.tgz", + "integrity": "sha512-QdXO3nXOzZB4pAjM0n6ZE+R9/+kPpECA/XSELIcc54NeYVnBqIk+4DFiBgK+8QbV3mdvTG6nedl7dTYgO+5wDw==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -10365,7 +10307,6 @@ } ], "license": "MIT", - "optional": true, "dependencies": { "strnum": "^2.1.0" }, @@ -10686,10 +10627,13 @@ } }, "node_modules/form-data-encoder": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.1.tgz", - "integrity": "sha512-EFRDrsMm/kyqbTQocNvRXMLjc7Es2Vk+IQFx/YW7hkUH1eBl4J1fqiP34l74Yt0pFLCNpc06fkbVk00008mzjg==", - "license": "MIT" + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "license": "MIT", + "engines": { + "node": ">= 14.17" + } }, "node_modules/formdata-node": { "version": "6.0.3", @@ -10894,171 +10838,42 @@ } }, "node_modules/gamedig": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/gamedig/-/gamedig-4.3.2.tgz", - "integrity": "sha512-TjYwybvy8HNAhkv2EJccd5HROIiMeMriWmeX8vT8m5Ibat5JMzVpugzsD8L8XZVrOfiXnVg/9DhWYM8k/VG/vw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gamedig/-/gamedig-5.3.2.tgz", + "integrity": "sha512-R2b1LwjW783PZsHRl9M8R06UkvJwXmJ6PDKk48UukJQ9ktiwrCeAk90MAZx6nF3oA444uf7r5eHjfaYbNoSV+Q==", "license": "MIT", "dependencies": { - "cheerio": "1.0.0-rc.10", + "fast-xml-parser": "5.2.5", "gbxremote": "0.2.1", - "got": "12.1.0", - "iconv-lite": "0.6.3", - "long": "5.2.0", - "minimist": "1.2.6", - "punycode": "2.1.1", + "got": "13.0.0", + "iconv-lite": "0.7.0", + "long": "5.3.2", + "minimist": "1.2.8", "seek-bzip": "2.0.0", + "telnet-client": "2.2.6", "varint": "6.0.0" }, "bin": { "gamedig": "bin/gamedig.js" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.20.0" } }, - "node_modules/gamedig/node_modules/cheerio": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", - "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", + "node_modules/gamedig/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { - "cheerio-select": "^1.5.0", - "dom-serializer": "^1.3.2", - "domhandler": "^4.2.0", - "htmlparser2": "^6.1.0", - "parse5": "^6.0.1", - "parse5-htmlparser2-tree-adapter": "^6.0.1", - "tslib": "^2.2.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 6" + "node": ">=0.10.0" }, "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/gamedig/node_modules/cheerio-select": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.6.0.tgz", - "integrity": "sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==", - "license": "BSD-2-Clause", - "dependencies": { - "css-select": "^4.3.0", - "css-what": "^6.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.3.1", - "domutils": "^2.8.0" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/gamedig/node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/gamedig/node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/gamedig/node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/gamedig/node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/gamedig/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/gamedig/node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/gamedig/node_modules/long": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz", - "integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w==", - "license": "Apache-2.0" - }, - "node_modules/gamedig/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "license": "MIT" - }, - "node_modules/gamedig/node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "license": "MIT", - "dependencies": { - "parse5": "^6.0.1" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/gauge": { @@ -11415,27 +11230,25 @@ } }, "node_modules/got": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/got/-/got-12.1.0.tgz", - "integrity": "sha512-hBv2ty9QN2RdbJJMK3hesmSkFTjVIHyIDDbssCKnSmq62edGgImJWD10Eb1k77TiV1bxloxqcFAVK8+9pkhOig==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", + "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", "license": "MIT", "dependencies": { - "@sindresorhus/is": "^4.6.0", + "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", - "@types/cacheable-request": "^6.0.2", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^6.0.4", - "cacheable-request": "^7.0.2", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", - "form-data-encoder": "1.7.1", + "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", - "responselike": "^2.0.0" + "responselike": "^3.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=16" }, "funding": { "url": "https://github.com/sindresorhus/got?sponsor=1" @@ -13623,12 +13436,15 @@ } }, "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", "license": "MIT", "engines": { - "node": ">=4" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/minimalistic-assert": { @@ -13653,10 +13469,13 @@ } }, "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "license": "MIT" + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/minimist-options": { "version": "4.1.0", @@ -14107,6 +13926,12 @@ "node": ">= 0.6" } }, + "node_modules/net": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz", + "integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==", + "license": "MIT" + }, "node_modules/net-snmp": { "version": "3.26.0", "resolved": "https://registry.npmjs.org/net-snmp/-/net-snmp-3.26.0.tgz", @@ -14388,12 +14213,12 @@ } }, "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -16272,26 +16097,20 @@ } }, "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", "license": "MIT", "dependencies": { - "lowercase-keys": "^2.0.0" + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/responselike/node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/retimer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/retimer/-/retimer-3.0.0.tgz", @@ -17254,6 +17073,15 @@ "node": ">= 0.4" } }, + "node_modules/stream": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz", + "integrity": "sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g==", + "license": "MIT", + "dependencies": { + "emitter-component": "^1.1.1" + } + }, "node_modules/stream-shift": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", @@ -17552,8 +17380,7 @@ "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/style-search": { "version": "0.1.0", @@ -17969,6 +17796,20 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/telnet-client": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/telnet-client/-/telnet-client-2.2.6.tgz", + "integrity": "sha512-ZUYrLsPtQupQww3eSEORDVOb6ztdtKEghya6TVXPo2tg/UQq2pn5rHhvwuUvyYpbnsoqdNY1fyD1GNkXHR8dYA==", + "license": "MIT", + "dependencies": { + "net": "^1.0.2", + "stream": "^0.0.2" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + } + }, "node_modules/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", @@ -19130,16 +18971,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/wait-on/node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/web-push": { "version": "3.6.7", "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz", diff --git a/package.json b/package.json index 987b5af52..09811f1ad 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "express-static-gzip": "~2.1.7", "feed": "^4.2.2", "form-data": "~4.0.0", - "gamedig": "^4.2.0", + "gamedig": "^5.0.1", "html-escaper": "^3.0.3", "http-cookie-agent": "~5.0.4", "http-graceful-shutdown": "~3.1.7", diff --git a/server/analytics/analytics.js b/server/analytics/analytics.js new file mode 100644 index 000000000..229d463e0 --- /dev/null +++ b/server/analytics/analytics.js @@ -0,0 +1,48 @@ +const googleAnalytics = require("./google-analytics"); +const umamiAnalytics = require("./umami-analytics"); +const plausibleAnalytics = require("./plausible-analytics"); +const matomoAnalytics = require("./matomo-analytics"); + +/** + * Returns a string that represents the javascript that is required to insert the selected Analytics' script + * into a webpage. + * @param {typeof import("../model/status_page").StatusPage} statusPage Status page populate HTML with + * @returns {string} HTML script tags to inject into page + */ +function getAnalyticsScript(statusPage) { + switch (statusPage.analyticsType) { + case "google": + return googleAnalytics.getGoogleAnalyticsScript(statusPage.analyticsId); + case "umami": + return umamiAnalytics.getUmamiAnalyticsScript(statusPage.analyticsScriptUrl, statusPage.analyticsId); + case "plausible": + return plausibleAnalytics.getPlausibleAnalyticsScript(statusPage.analyticsScriptUrl, statusPage.analyticsId); + case "matomo": + return matomoAnalytics.getMatomoAnalyticsScript(statusPage.analyticsScriptUrl, statusPage.analyticsId); + default: + return null; + } +} + +/** + * Function that checks wether the selected analytics has been configured properly + * @param {typeof import("../model/status_page").StatusPage} statusPage Status page populate HTML with + * @returns {boolean} Boolean defining if the analytics config is valid + */ +function isValidAnalyticsConfig(statusPage) { + switch (statusPage.analyticsType) { + case "google": + return statusPage.analyticsId != null; + case "umami": + case "plausible": + case "matomo": + return statusPage.analyticsId != null && statusPage.analyticsScriptUrl != null; + default: + return false; + } +} + +module.exports = { + getAnalyticsScript, + isValidAnalyticsConfig +}; diff --git a/server/google-analytics.js b/server/analytics/google-analytics.js similarity index 100% rename from server/google-analytics.js rename to server/analytics/google-analytics.js diff --git a/server/analytics/matomo-analytics.js b/server/analytics/matomo-analytics.js new file mode 100644 index 000000000..fdc009e63 --- /dev/null +++ b/server/analytics/matomo-analytics.js @@ -0,0 +1,47 @@ +const jsesc = require("jsesc"); +const { escape } = require("html-escaper"); + +/** + * Returns a string that represents the javascript that is required to insert the Matomo Analytics script + * into a webpage. + * @param {string} matomoUrl Domain name with tld to use with the Matomo Analytics script. + * @param {string} siteId Site ID to use with the Matomo Analytics script. + * @returns {string} HTML script tags to inject into page + */ +function getMatomoAnalyticsScript(matomoUrl, siteId) { + let escapedMatomoUrlJS = jsesc(matomoUrl, { isScriptContext: true }); + let escapedSiteIdJS = jsesc(siteId, { isScriptContext: true }); + + if (escapedMatomoUrlJS) { + escapedMatomoUrlJS = escapedMatomoUrlJS.trim(); + } + + if (escapedSiteIdJS) { + escapedSiteIdJS = escapedSiteIdJS.trim(); + } + + // Escape the domain url for use in an HTML attribute. + let escapedMatomoUrlHTMLAttribute = escape(escapedMatomoUrlJS); + + // Escape the website id for use in an HTML attribute. + let escapedSiteIdHTMLAttribute = escape(escapedSiteIdJS); + + return ` + + `; +} + +module.exports = { + getMatomoAnalyticsScript, +}; diff --git a/server/analytics/plausible-analytics.js b/server/analytics/plausible-analytics.js new file mode 100644 index 000000000..131f1136b --- /dev/null +++ b/server/analytics/plausible-analytics.js @@ -0,0 +1,36 @@ +const jsesc = require("jsesc"); +const { escape } = require("html-escaper"); + +/** + * Returns a string that represents the javascript that is required to insert the Plausible Analytics script + * into a webpage. + * @param {string} scriptUrl the Plausible Analytics script url. + * @param {string} domainsToMonitor Domains to track seperated by a ',' to add Plausible Analytics script. + * @returns {string} HTML script tags to inject into page + */ +function getPlausibleAnalyticsScript(scriptUrl, domainsToMonitor) { + let escapedScriptUrlJS = jsesc(scriptUrl, { isScriptContext: true }); + let escapedWebsiteIdJS = jsesc(domainsToMonitor, { isScriptContext: true }); + + if (escapedScriptUrlJS) { + escapedScriptUrlJS = escapedScriptUrlJS.trim(); + } + + if (escapedWebsiteIdJS) { + escapedWebsiteIdJS = escapedWebsiteIdJS.trim(); + } + + // Escape the domain url for use in an HTML attribute. + let escapedScriptUrlHTMLAttribute = escape(escapedScriptUrlJS); + + // Escape the website id for use in an HTML attribute. + let escapedWebsiteIdHTMLAttribute = escape(escapedWebsiteIdJS); + + return ` + + `; +} + +module.exports = { + getPlausibleAnalyticsScript +}; diff --git a/server/analytics/umami-analytics.js b/server/analytics/umami-analytics.js new file mode 100644 index 000000000..48c8b2eca --- /dev/null +++ b/server/analytics/umami-analytics.js @@ -0,0 +1,36 @@ +const jsesc = require("jsesc"); +const { escape } = require("html-escaper"); + +/** + * Returns a string that represents the javascript that is required to insert the Umami Analytics script + * into a webpage. + * @param {string} scriptUrl the Umami Analytics script url. + * @param {string} websiteId Website ID to use with the Umami Analytics script. + * @returns {string} HTML script tags to inject into page + */ +function getUmamiAnalyticsScript(scriptUrl, websiteId) { + let escapedScriptUrlJS = jsesc(scriptUrl, { isScriptContext: true }); + let escapedWebsiteIdJS = jsesc(websiteId, { isScriptContext: true }); + + if (escapedScriptUrlJS) { + escapedScriptUrlJS = escapedScriptUrlJS.trim(); + } + + if (escapedWebsiteIdJS) { + escapedWebsiteIdJS = escapedWebsiteIdJS.trim(); + } + + // Escape the Script url for use in an HTML attribute. + let escapedScriptUrlHTMLAttribute = escape(escapedScriptUrlJS); + + // Escape the website id for use in an HTML attribute. + let escapedWebsiteIdHTMLAttribute = escape(escapedWebsiteIdJS); + + return ` + + `; +} + +module.exports = { + getUmamiAnalyticsScript, +}; diff --git a/server/model/status_page.js b/server/model/status_page.js index 0906561bf..55b9f72c7 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -3,7 +3,7 @@ const { R } = require("redbean-node"); const cheerio = require("cheerio"); const { UptimeKumaServer } = require("../uptime-kuma-server"); const jsesc = require("jsesc"); -const googleAnalytics = require("../google-analytics"); +const analytics = require("../analytics/analytics"); const { marked } = require("marked"); const { Feed } = require("feed"); const config = require("../config"); @@ -121,9 +121,9 @@ class StatusPage extends BeanModel { const head = $("head"); - if (statusPage.google_analytics_tag_id) { - let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.google_analytics_tag_id); - head.append($(escapedGoogleAnalyticsScript)); + if (analytics.isValidAnalyticsConfig(statusPage)) { + let escapedAnalyticsScript = analytics.getAnalyticsScript(statusPage); + head.append($(escapedAnalyticsScript)); } // OG Meta Tags @@ -408,7 +408,9 @@ class StatusPage extends BeanModel { customCSS: this.custom_css, footerText: this.footer_text, showPoweredBy: !!this.show_powered_by, - googleAnalyticsId: this.google_analytics_tag_id, + analyticsId: this.analytics_id, + analyticsScriptUrl: this.analytics_script_url, + analyticsType: this.analytics_type, showCertificateExpiry: !!this.show_certificate_expiry, showOnlyLastHeartbeat: !!this.show_only_last_heartbeat }; @@ -432,7 +434,9 @@ class StatusPage extends BeanModel { customCSS: this.custom_css, footerText: this.footer_text, showPoweredBy: !!this.show_powered_by, - googleAnalyticsId: this.google_analytics_tag_id, + analyticsId: this.analytics_id, + analyticsScriptUrl: this.analytics_script_url, + analyticsType: this.analytics_type, showCertificateExpiry: !!this.show_certificate_expiry, showOnlyLastHeartbeat: !!this.show_only_last_heartbeat }; diff --git a/server/monitor-types/gamedig.js b/server/monitor-types/gamedig.js index b20c2435c..35a015f98 100644 --- a/server/monitor-types/gamedig.js +++ b/server/monitor-types/gamedig.js @@ -1,6 +1,6 @@ const { MonitorType } = require("./monitor-type"); -const { UP, DOWN } = require("../../src/util"); -const Gamedig = require("gamedig"); +const { UP } = require("../../src/util"); +const { GameDig } = require("gamedig"); const dns = require("dns").promises; const net = require("net"); @@ -11,15 +11,13 @@ class GameDigMonitorType extends MonitorType { * @inheritdoc */ async check(monitor, heartbeat, server) { - heartbeat.status = DOWN; - let host = monitor.hostname; if (net.isIP(host) === 0) { host = await this.resolveHostname(host); } try { - const state = await Gamedig.query({ + const state = await GameDig.query({ type: monitor.game, host: host, port: monitor.port, diff --git a/server/socket-handlers/general-socket-handler.js b/server/socket-handlers/general-socket-handler.js index b996efe7b..ad57d7b1e 100644 --- a/server/socket-handlers/general-socket-handler.js +++ b/server/socket-handlers/general-socket-handler.js @@ -2,30 +2,35 @@ const { log } = require("../../src/util"); const { Settings } = require("../settings"); const { sendInfo } = require("../client"); const { checkLogin } = require("../util-server"); -const GameResolver = require("gamedig/lib/GameResolver"); +const { games } = require("gamedig"); const { testChrome } = require("../monitor-types/real-browser-monitor-type"); const fsAsync = require("fs").promises; const path = require("path"); -let gameResolver = new GameResolver(); -let gameList = null; - /** * Get a game list via GameDig - * @returns {object[]} list of games supported by GameDig + * @returns {object} list of games supported by GameDig */ function getGameList() { - if (gameList == null) { - gameList = gameResolver._readGames().games.sort((a, b) => { - if ( a.pretty < b.pretty ) { - return -1; - } - if ( a.pretty > b.pretty ) { - return 1; - } - return 0; - }); - } + let gameList = []; + gameList = Object.keys(games).map(key => { + const item = games[key]; + return { + keys: [ key ], + pretty: item.name, + options: item.options, + extra: item.extra || {} + }; + }); + gameList.sort((a, b) => { + if ( a.pretty < b.pretty ) { + return -1; + } + if ( a.pretty > b.pretty ) { + return 1; + } + return 0; + }); return gameList; } diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js index a90220ea0..089b00f3c 100644 --- a/server/socket-handlers/status-page-socket-handler.js +++ b/server/socket-handlers/status-page-socket-handler.js @@ -365,7 +365,9 @@ module.exports.statusPageSocketHandler = (socket) => { statusPage.show_only_last_heartbeat = config.showOnlyLastHeartbeat; statusPage.show_certificate_expiry = config.showCertificateExpiry; statusPage.modified_date = R.isoDateTime(); - statusPage.google_analytics_tag_id = config.googleAnalyticsId; + statusPage.analytics_id = config.analyticsId; + statusPage.analytics_script_url = config.analyticsScriptUrl; + statusPage.analytics_type = config.analyticsType; await R.store(statusPage); diff --git a/src/lang/en.json b/src/lang/en.json index c9e60151f..f89b92fbe 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -867,6 +867,9 @@ "wayToGetClickSendSMSToken": "You can get API Username and API Key from {here}.", "Custom Monitor Type": "Custom Monitor Type", "Google Analytics ID": "Google Analytics ID", + "Analytics Type": "Analytics Type", + "Analytics ID": "Analytics ID", + "Analytics Script URL": "Analytics Script URL", "Edit Tag": "Edit Tag", "Server Address": "Server Address", "Learn More": "Learn More", @@ -1216,6 +1219,10 @@ "Phone numbers": "Phone numbers", "Sender name": "Sender name", "smsplanetNeedToApproveName": "Needs to be approved in the client panel", + "Google": "Google", + "Plausible": "Plausible", + "Matomo": "Matomo", + "Umami": "Umami", "Disable URL in Notification": "Disable URL in Notification", "Ip Family": "IP Family", "ipFamilyDescriptionAutoSelect": "Uses the {happyEyeballs} for determining the IP family.", diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index b86d55eca..d5f76793c 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -98,10 +98,27 @@ - + +
- - + + +
+ +
+ + +
+ +
+ +
diff --git a/test/backend-test/monitors/test-gamedig.js b/test/backend-test/monitors/test-gamedig.js index 57e7a89dc..5274e69dc 100644 --- a/test/backend-test/monitors/test-gamedig.js +++ b/test/backend-test/monitors/test-gamedig.js @@ -1,15 +1,15 @@ const { describe, test, mock } = require("node:test"); const assert = require("node:assert"); const { GameDigMonitorType } = require("../../../server/monitor-types/gamedig"); -const { UP, DOWN, PENDING } = require("../../../src/util"); +const { UP, PENDING } = require("../../../src/util"); const net = require("net"); -const Gamedig = require("gamedig"); +const { GameDig } = require("gamedig"); describe("GameDig Monitor", () => { test("check() sets status to UP when Gamedig.query returns valid server response", async () => { const gamedigMonitor = new GameDigMonitorType(); - mock.method(Gamedig, "query", async () => { + mock.method(GameDig, "query", async () => { return { name: "Test Minecraft Server", ping: 42, @@ -43,7 +43,7 @@ describe("GameDig Monitor", () => { test("check() resolves hostname to IP address when hostname is not an IP", async () => { const gamedigMonitor = new GameDigMonitorType(); - mock.method(Gamedig, "query", async (options) => { + mock.method(GameDig, "query", async (options) => { assert.ok( net.isIP(options.host) !== 0, `Expected IP address, got ${options.host}` @@ -82,7 +82,7 @@ describe("GameDig Monitor", () => { let capturedOptions = null; - mock.method(Gamedig, "query", async (options) => { + mock.method(GameDig, "query", async (options) => { capturedOptions = options; return { name: "Test Server", @@ -117,7 +117,7 @@ describe("GameDig Monitor", () => { let capturedOptions = null; - mock.method(Gamedig, "query", async (options) => { + mock.method(GameDig, "query", async (options) => { capturedOptions = options; return { name: "Test Server", @@ -152,7 +152,7 @@ describe("GameDig Monitor", () => { let capturedOptions = null; - mock.method(Gamedig, "query", async (options) => { + mock.method(GameDig, "query", async (options) => { capturedOptions = options; return { name: "Test Server", @@ -189,7 +189,7 @@ describe("GameDig Monitor", () => { let capturedOptions = null; - mock.method(Gamedig, "query", async (options) => { + mock.method(GameDig, "query", async (options) => { capturedOptions = options; return { name: "Test Server", @@ -219,7 +219,7 @@ describe("GameDig Monitor", () => { } }); - test("check() sets status to DOWN and rejects when game server is unreachable", async () => { + test("check() rejects when game server is unreachable", async () => { const gamedigMonitor = new GameDigMonitorType(); const monitor = { @@ -238,8 +238,6 @@ describe("GameDig Monitor", () => { gamedigMonitor.check(monitor, heartbeat, {}), /Error/ ); - - assert.strictEqual(heartbeat.status, DOWN); }); test("resolveHostname() returns IP address when given valid hostname", async () => { diff --git a/test/e2e/specs/status-page.spec.js b/test/e2e/specs/status-page.spec.js index 1b86d5137..809cfdbe4 100644 --- a/test/e2e/specs/status-page.spec.js +++ b/test/e2e/specs/status-page.spec.js @@ -24,6 +24,12 @@ test.describe("Status Page", () => { const refreshInterval = 30; const theme = "dark"; const googleAnalyticsId = "G-123"; + const umamiAnalyticsScriptUrl = "https://umami.example.com/script.js"; + const umamiAnalyticsWebsiteId = "606487e2-bc25-45f9-9132-fa8b065aad46"; + const plausibleAnalyticsScriptUrl = "https://plausible.example.com/js/script.js"; + const plausibleAnalyticsDomainsUrls = "one.com,two.com"; + const matomoUrl = "https://matomoto.example.com"; + const matomoSiteId = "123456789"; const customCss = "body { background: rgb(0, 128, 128) !important; }"; const descriptionText = "This is an example status page."; const incidentTitle = "Example Outage Incident"; @@ -77,7 +83,8 @@ test.describe("Status Page", () => { await page.getByTestId("show-tags-checkbox").uncheck(); await page.getByTestId("show-powered-by-checkbox").uncheck(); await page.getByTestId("show-certificate-expiry-checkbox").uncheck(); - await page.getByTestId("google-analytics-input").fill(googleAnalyticsId); + await page.getByTestId("analytics-type-select").selectOption("google"); + await page.getByTestId("analytics-id-input").fill(googleAnalyticsId); await page.getByTestId("custom-css-input").getByTestId("textarea").fill(customCss); // Prism // Add an incident @@ -136,6 +143,7 @@ test.describe("Status Page", () => { expect(backgroundColor).toEqual("rgb(0, 128, 128)"); await screenshot(testInfo, page); + expect(await page.locator("head").innerHTML()).toContain(googleAnalyticsId); // Flip the "Show Tags" and "Show Powered By" switches: await page.getByTestId("edit-button").click(); @@ -144,6 +152,11 @@ test.describe("Status Page", () => { await page.getByTestId("show-powered-by-checkbox").setChecked(true); await screenshot(testInfo, page); + + // Fill in umami analytics after editing + await page.getByTestId("analytics-type-select").selectOption("umami"); + await page.getByTestId("analytics-script-url-input").fill(umamiAnalyticsScriptUrl); + await page.getByTestId("analytics-id-input").fill(umamiAnalyticsWebsiteId); await page.getByTestId("save-button").click(); await expect(page.getByTestId("edit-sidebar")).toHaveCount(0); @@ -154,6 +167,29 @@ test.describe("Status Page", () => { await expect(page.getByTestId("monitor-tag").filter({ hasText: tagValue2 })).toBeVisible(); await screenshot(testInfo, page); + + expect(await page.locator("head").innerHTML()).toContain(umamiAnalyticsScriptUrl); + expect(await page.locator("head").innerHTML()).toContain(umamiAnalyticsWebsiteId); + + await page.getByTestId("edit-button").click(); + // Fill in plausible analytics after editing + await page.getByTestId("analytics-type-select").selectOption("plausible"); + await page.getByTestId("analytics-script-url-input").fill(plausibleAnalyticsScriptUrl); + await page.getByTestId("analytics-id-input").fill(plausibleAnalyticsDomainsUrls); + await page.getByTestId("save-button").click(); + await screenshot(testInfo, page); + expect(await page.locator("head").innerHTML()).toContain(plausibleAnalyticsScriptUrl); + expect(await page.locator("head").innerHTML()).toContain(plausibleAnalyticsDomainsUrls); + + await page.getByTestId("edit-button").click(); + // Fill in matomo analytics after editing + await page.getByTestId("analytics-type-select").selectOption("matomo"); + await page.getByTestId("analytics-script-url-input").fill(matomoUrl); + await page.getByTestId("analytics-id-input").fill(matomoSiteId); + await page.getByTestId("save-button").click(); + await screenshot(testInfo, page); + expect(await page.locator("head").innerHTML()).toContain(matomoUrl); + expect(await page.locator("head").innerHTML()).toContain(matomoSiteId); }); // @todo Test certificate expiry