From b8b8bdc9353de128a816e1a2154735ca56f914f6 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 25 Dec 2025 03:29:19 +0100 Subject: [PATCH 1/5] add meta tags to the status page --- package-lock.json | 532 ++++++++++++++++++ package.json | 1 + server/model/status_page.js | 101 +++- server/routers/status-page-router.js | 30 + server/utils/og-image.js | 340 +++++++++++ .../monitor-conditions/test-evaluator.js | 74 +-- .../monitor-conditions/test-operators.js | 178 +++--- .../snapshots/og-images/all-down.svg | 33 ++ .../all-up-custom-icon-no-branding.svg | 44 ++ .../all-up-custom-icon-with-branding.svg | 45 ++ .../snapshots/og-images/all-up-long-title.svg | 33 ++ .../og-images/all-up-many-monitors.svg | 41 ++ .../og-images/all-up-no-branding.svg | 32 ++ .../snapshots/og-images/all-up.svg | 33 ++ .../snapshots/og-images/maintenance.svg | 28 + .../snapshots/og-images/partial.svg | 37 ++ .../backend-test/test-status-page-og-image.js | 401 +++++++++++++ test/e2e/specs/status-page.spec.js | 37 ++ 18 files changed, 1892 insertions(+), 128 deletions(-) create mode 100644 server/utils/og-image.js create mode 100644 test/backend-test/snapshots/og-images/all-down.svg create mode 100644 test/backend-test/snapshots/og-images/all-up-custom-icon-no-branding.svg create mode 100644 test/backend-test/snapshots/og-images/all-up-custom-icon-with-branding.svg create mode 100644 test/backend-test/snapshots/og-images/all-up-long-title.svg create mode 100644 test/backend-test/snapshots/og-images/all-up-many-monitors.svg create mode 100644 test/backend-test/snapshots/og-images/all-up-no-branding.svg create mode 100644 test/backend-test/snapshots/og-images/all-up.svg create mode 100644 test/backend-test/snapshots/og-images/maintenance.svg create mode 100644 test/backend-test/snapshots/og-images/partial.svg create mode 100644 test/backend-test/test-status-page-og-image.js diff --git a/package-lock.json b/package-lock.json index 8c0506ba1..13ab3f528 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,6 +78,7 @@ "redbean-node": "~0.3.0", "redis": "~5.9.0", "semver": "~7.5.4", + "sharp": "~0.34.5", "socket.io": "~4.8.0", "socket.io-client": "~4.8.0", "socks-proxy-agent": "~8.0.5", @@ -2914,6 +2915,16 @@ "postcss-selector-parser": "^6.0.13" } }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@es-joy/jsdoccomment": { "version": "0.40.1", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", @@ -3603,6 +3614,471 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@intlify/core-base": { "version": "9.14.5", "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz", @@ -16605,6 +17081,62 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 6088015b3..da123b6dd 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,7 @@ "redbean-node": "~0.3.0", "redis": "~5.9.0", "semver": "~7.5.4", + "sharp": "~0.34.5", "socket.io": "~4.8.0", "socket.io-client": "~4.8.0", "socks-proxy-agent": "~8.0.5", diff --git a/server/model/status_page.js b/server/model/status_page.js index 224441127..c3a0a06fe 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -7,6 +7,8 @@ const googleAnalytics = require("../google-analytics"); const { marked } = require("marked"); const { Feed } = require("feed"); const config = require("../config"); +const { setting } = require("../util-server"); +const { generateOGImage, getStatusColor } = require("../utils/og-image"); const { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE, DOWN } = require("../../src/util"); @@ -93,6 +95,22 @@ class StatusPage extends BeanModel { return feed.rss2(); } + /** + * Helper function to create and append meta tags + * @param {object} $ Cheerio instance + * @param {object} head The head element + * @param {string} property The property name (or name for non-OG tags) + * @param {string} content The content value + * @param {boolean} isOG Whether this is an Open Graph tag (default: true) + * @returns {void} + * @private + */ + static _appendMetaTag($, head, property, content, isOG = true) { + const attr = isOG ? "property" : "name"; + const meta = $(``).attr(attr, property).attr("content", content); + head.append(meta); + } + /** * SSR for status pages * @param {string} indexHTML HTML page to render @@ -126,11 +144,30 @@ class StatusPage extends BeanModel { } // OG Meta Tags - let ogTitle = $("").attr("content", statusPage.title); - head.append(ogTitle); + StatusPage._appendMetaTag($, head, "og:title", statusPage.title); + StatusPage._appendMetaTag($, head, "og:description", description155); + StatusPage._appendMetaTag($, head, "og:type", "website"); - let ogDescription = $("").attr("content", description155); - head.append(ogDescription); + // Add og:url if primaryBaseURL is configured + const primaryBaseURL = await setting("primaryBaseURL"); + if (primaryBaseURL) { + StatusPage._appendMetaTag($, head, "og:url", `${primaryBaseURL}/status/${statusPage.slug}`); + } + + // og:image needs an absolute URL to work + if (primaryBaseURL) { + const imageUrl = `${primaryBaseURL}/api/status-page/${statusPage.slug}/image`; + StatusPage._appendMetaTag($, head, "og:image", imageUrl); + StatusPage._appendMetaTag($, head, "og:image:width", "1200"); + StatusPage._appendMetaTag($, head, "og:image:height", "630"); + StatusPage._appendMetaTag($, head, "og:image:alt", `${statusPage.title} - Status Page`); + StatusPage._appendMetaTag($, head, "twitter:image", imageUrl, false); + } + + // Twitter Card Meta Tags + StatusPage._appendMetaTag($, head, "twitter:card", "summary_large_image", false); + StatusPage._appendMetaTag($, head, "twitter:title", statusPage.title, false); + StatusPage._appendMetaTag($, head, "twitter:description", description155, false); // Preload data // Add jsesc, fix https://github.com/louislam/uptime-kuma/issues/2186 @@ -210,6 +247,62 @@ class StatusPage extends BeanModel { return "?"; } + /** + * Generate an Open Graph image for the status page + * @param {StatusPage} statusPage Status page object + * @returns {Promise} PNG image buffer + */ + static async generateOGImage(statusPage) { + // Get status data + const { heartbeats, statusDescription } = await StatusPage.getRSSPageData(statusPage); + const status = StatusPage.overallStatus(heartbeats); + const statusColor = getStatusColor(status); + + // Collect monitor information for display (top monitors with their status) + const monitors = []; + + // Try to get full status page data if available (for real instances) + // This will gracefully fail for mock objects in tests + try { + if (statusPage.toPublicJSON && statusPage.id) { + const statusPageData = await StatusPage.getStatusPageData(statusPage); + if (statusPageData.publicGroupList) { + for (const group of statusPageData.publicGroupList) { + if (group.monitorList) { + for (const monitor of group.monitorList) { + monitors.push({ + name: monitor.name, + status: monitor.status || 2 // 1=up, 0=down, 2=pending + }); + } + } + } + } + } + } catch (error) { + // If getting monitor details fails, continue without them + // This allows the function to work with mock objects + } + + // Get icon - use getIcon() method if available, otherwise use icon property + const icon = (statusPage.getIcon && typeof statusPage.getIcon === "function") + ? statusPage.getIcon() + : statusPage.icon; + + // If no detailed monitor data, create array with count for display + const monitorsForDisplay = monitors.length > 0 ? monitors : heartbeats.map(() => ({})); + + // Generate OG image using utility function + return await generateOGImage({ + title: statusPage.title, + statusDescription, + statusColor, + icon, + showPoweredBy: !!statusPage.show_powered_by, + monitors: monitorsForDisplay + }); + } + /** * Get all data required for RSS * @param {StatusPage} statusPage Status page to get data for diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index 6e57451f1..02085b6e4 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -244,4 +244,34 @@ router.get("/api/status-page/:slug/badge", cache("5 minutes"), async (request, r } }); +// Status page Open Graph image +router.get("/api/status-page/:slug/image", cache("5 minutes"), async (request, response) => { + allowDevAllOrigin(response); + let slug = request.params.slug; + slug = slug.toLowerCase(); + + try { + // Get Status Page + let statusPage = await R.findOne("status_page", " slug = ? ", [ + slug + ]); + + if (!statusPage) { + sendHttpError(response, "Status Page Not Found"); + return; + } + + // Generate PNG image + const pngBuffer = await StatusPage.generateOGImage(statusPage); + + // Set appropriate headers + response.type("image/png"); + response.setHeader("Cache-Control", "public, max-age=300"); // 5 minutes + response.send(pngBuffer); + + } catch (error) { + sendHttpError(response, error.message); + } +}); + module.exports = router; diff --git a/server/utils/og-image.js b/server/utils/og-image.js new file mode 100644 index 000000000..35199af84 --- /dev/null +++ b/server/utils/og-image.js @@ -0,0 +1,340 @@ +const sharp = require("sharp"); +const fs = require("fs"); +const path = require("path"); + +const { + STATUS_PAGE_ALL_DOWN, + STATUS_PAGE_ALL_UP, + STATUS_PAGE_PARTIAL_DOWN, + MAINTENANCE, + UP, + DOWN, + PENDING +} = require("../util-server"); + +// Image dimensions (Open Graph standard) +const OG_IMAGE_WIDTH = 1200; +const OG_IMAGE_HEIGHT = 630; + +// Display limits +const MAX_TITLE_LENGTH = 40; +const MAX_MONITOR_NAME_LENGTH = 40; +const MAX_INDIVIDUAL_MONITORS = 3; + +/** + * Get status color based on status code + * @param {number} status Status code + * @returns {string} Hex color code + */ +function getStatusColor(status) { + switch (status) { + case STATUS_PAGE_ALL_UP: + return "#10b981"; // green + case STATUS_PAGE_PARTIAL_DOWN: + return "#f59e0b"; // amber + case STATUS_PAGE_ALL_DOWN: + return "#ef4444"; // red + case MAINTENANCE: + return "#3b82f6"; // blue + default: + return "#6b7280"; // grey + } +} + +/** + * Get monitor status color + * @param {number} status Monitor status code + * @returns {string} Hex color code + */ +function getMonitorStatusColor(status) { + switch (status) { + case UP: + return "#10b981"; // green + case DOWN: + return "#ef4444"; // red + case MAINTENANCE: + return "#3b82f6"; // blue + case PENDING: + return "#f59e0b"; // amber + default: + return "#6b7280"; // grey + } +} + +/** + * Escape XML special characters + * @param {string} text Text to escape + * @returns {string} Escaped text + */ +function escapeXml(text) { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +/** + * Truncate text with ellipsis if too long + * @param {string} text Text to truncate + * @param {number} maxLength Maximum length + * @returns {string} Truncated text + */ +function truncateText(text, maxLength) { + if (text.length <= maxLength) { + return text; + } + return text.substring(0, maxLength - 3) + "..."; +} + +/** + * Count monitors by status + * @param {Array} monitors Array of monitor objects + * @returns {object} Status counts + */ +function countMonitorsByStatus(monitors) { + const counts = { up: 0, + down: 0, + pending: 0, + maintenance: 0 }; + + monitors.forEach((monitor) => { + if (monitor.status === UP) { + counts.up++; + } else if (monitor.status === DOWN) { + counts.down++; + } else if (monitor.status === PENDING) { + counts.pending++; + } else if (monitor.status === MAINTENANCE) { + counts.maintenance++; + } + }); + + return counts; +} + +/** + * Generate SVG for monitor status summary (when more than 3 monitors) + * @param {object} statusCounts Status counts object + * @param {number} startY Starting Y coordinate + * @returns {string} SVG markup + */ +function generateMonitorStatusSummary(statusCounts, startY) { + let y = startY; + let svg = ""; + + if (statusCounts.up > 0) { + svg += ` + + ${statusCounts.up} Up`; + y += 35; + } + + if (statusCounts.down > 0) { + svg += ` + + ${statusCounts.down} Down`; + y += 35; + } + + if (statusCounts.maintenance > 0) { + svg += ` + + ${statusCounts.maintenance} Maintenance`; + y += 35; + } + + if (statusCounts.pending > 0) { + svg += ` + + ${statusCounts.pending} Pending`; + } + + return svg; +} + +/** + * Generate SVG for individual monitor details (3 or fewer monitors) + * @param {Array} monitors Array of monitor objects + * @param {number} startY Starting Y coordinate + * @returns {string} SVG markup + */ +function generateIndividualMonitorDetails(monitors, startY) { + let svg = ""; + const displayMonitors = monitors.slice(0, MAX_INDIVIDUAL_MONITORS); + + displayMonitors.forEach((monitor, index) => { + const y = startY + (index * 35); + const statusColor = getMonitorStatusColor(monitor.status); + const monitorName = truncateText(monitor.name, MAX_MONITOR_NAME_LENGTH); + + svg += ` + + ${escapeXml(monitorName)}`; + }); + + return svg; +} + +/** + * Generate monitor details section based on monitor data + * @param {Array} monitors Array of monitor objects + * @returns {string} SVG markup + */ +function generateMonitorDetailsSection(monitors) { + const startY = 420; + + // No monitors or monitors without names - show count + if (monitors.length === 0 || !monitors[0].name) { + const plural = monitors.length !== 1 ? "s" : ""; + return ` + ${monitors.length} Monitor${plural} tracked`; + } + + // Show summary for more than 3 monitors + if (monitors.length > MAX_INDIVIDUAL_MONITORS) { + const statusCounts = countMonitorsByStatus(monitors); + return generateMonitorStatusSummary(statusCounts, startY); + } + + // Show individual monitors for 3 or fewer + return generateIndividualMonitorDetails(monitors, startY); +} + +/** + * Load and embed icon SVG + * @param {string|null} icon Icon URL or path + * @returns {string} SVG markup for icon + */ +function generateIconSection(icon) { + if (!icon) { + return ""; + } + + try { + let filePath = ""; + + if (icon.startsWith("/upload/")) { + filePath = path.join(__dirname, "../../data", icon); + } else { + filePath = path.join(__dirname, "../../public/icon.svg"); + } + + if (!fs.existsSync(filePath) || !icon.endsWith(".svg")) { + return ""; + } + + const iconContent = fs.readFileSync(filePath, "utf8"); + const svgMatch = iconContent.match(/]*>([\s\S]*)<\/svg>/i); + + if (!svgMatch) { + return ""; + } + + // Embed icon scaled to ~80px at top right + return ` + + + ${svgMatch[1]} + `; + } catch (error) { + // Silently fail - OG image will generate without icon + return ""; + } +} + +/** + * Generate SVG for Open Graph image + * @param {string} title Status page title + * @param {string} statusDescription Status description text + * @param {string} statusColor Status color hex code + * @param {string|null} icon Custom icon URL + * @param {boolean} showPoweredBy Whether to show "Powered by Uptime Kuma" branding + * @param {number} timestamp Last updated timestamp (Unix timestamp in milliseconds) + * @param {Array} monitors Array of monitor objects with name and status + * @returns {string} SVG markup + */ +function generateOGImageSVG(title, statusDescription, statusColor, icon, showPoweredBy, timestamp, monitors) { + const displayTitle = truncateText(title, MAX_TITLE_LENGTH); + const iconSVG = generateIconSection(icon); + const monitorDetailsSVG = generateMonitorDetailsSection(monitors); + + // Footer + const date = new Date(timestamp); + const timestampText = `UPDATED AT ${date.toISOString()}`; + let footerSVG = ""; + + if (showPoweredBy) { + footerSVG += ` + POWERED BY UPTIME KUMA`; + } + + footerSVG += ` + ${timestampText}`; + + return ` + + + + + + +${iconSVG} + + ${escapeXml(displayTitle)} + + + + + + ${escapeXml(statusDescription)} + +${monitorDetailsSVG}${footerSVG} +`; +} + +/** + * Generate an Open Graph image for the status page + * @param {object} statusPageData Status page data object containing title, status info, monitors, etc. + * @param {string} statusPageData.title Status page title + * @param {string} statusPageData.statusDescription Status description text + * @param {string} statusPageData.statusColor Status color hex code + * @param {string|null} statusPageData.icon Custom icon URL + * @param {boolean} statusPageData.showPoweredBy Whether to show "Powered by Uptime Kuma" branding + * @param {Array} statusPageData.monitors Array of monitor objects + * @returns {Promise} PNG image buffer + */ +async function generateOGImage(statusPageData) { + const { + title, + statusDescription, + statusColor, + icon, + showPoweredBy, + monitors + } = statusPageData; + + const svg = generateOGImageSVG( + title, + statusDescription, + statusColor, + icon, + !!showPoweredBy, + Date.now(), + monitors + ); + + const pngBuffer = await sharp(Buffer.from(svg)) + .png() + .toBuffer(); + + return pngBuffer; +} + +module.exports = { + generateOGImageSVG, + generateOGImage, + getStatusColor, + escapeXml, +}; diff --git a/test/backend-test/monitor-conditions/test-evaluator.js b/test/backend-test/monitor-conditions/test-evaluator.js index da7c7fabf..5998751b4 100644 --- a/test/backend-test/monitor-conditions/test-evaluator.js +++ b/test/backend-test/monitor-conditions/test-evaluator.js @@ -3,44 +3,46 @@ const assert = require("node:assert"); const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("../../../server/monitor-conditions/expression.js"); const { evaluateExpressionGroup, evaluateExpression } = require("../../../server/monitor-conditions/evaluator.js"); -test("Test evaluateExpression", async (t) => { - const expr = new ConditionExpression("record", "contains", "mx1.example.com"); - assert.strictEqual(true, evaluateExpression(expr, { record: "mx1.example.com" })); - assert.strictEqual(false, evaluateExpression(expr, { record: "mx2.example.com" })); -}); +test("evaluateExpression", async (t) => { + await t.test("Test evaluateExpression", async (t) => { + const expr = new ConditionExpression("record", "contains", "mx1.example.com"); + assert.strictEqual(true, evaluateExpression(expr, { record: "mx1.example.com" })); + assert.strictEqual(false, evaluateExpression(expr, { record: "mx2.example.com" })); + }); -test("Test evaluateExpressionGroup with logical AND", async (t) => { - const group = new ConditionExpressionGroup([ - new ConditionExpression("record", "contains", "mx1."), - new ConditionExpression("record", "contains", "example.com", LOGICAL.AND), - ]); - assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" })); - assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." })); - assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" })); -}); + await t.test("Test evaluateExpressionGroup with logical AND", async (t) => { + const group = new ConditionExpressionGroup([ + new ConditionExpression("record", "contains", "mx1."), + new ConditionExpression("record", "contains", "example.com", LOGICAL.AND), + ]); + assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" })); + assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." })); + assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" })); + }); -test("Test evaluateExpressionGroup with logical OR", async (t) => { - const group = new ConditionExpressionGroup([ - new ConditionExpression("record", "contains", "example.com"), - new ConditionExpression("record", "contains", "example.org", LOGICAL.OR), - ]); - assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.com" })); - assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.org" })); - assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.net" })); -}); - -test("Test evaluateExpressionGroup with nested group", async (t) => { - const group = new ConditionExpressionGroup([ - new ConditionExpression("record", "contains", "mx1."), - new ConditionExpressionGroup([ + await t.test("Test evaluateExpressionGroup with logical OR", async (t) => { + const group = new ConditionExpressionGroup([ new ConditionExpression("record", "contains", "example.com"), new ConditionExpression("record", "contains", "example.org", LOGICAL.OR), - ]), - ]); - assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." })); - assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" })); - assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.org" })); - assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" })); - assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.org" })); - assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1.example.net" })); + ]); + assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.com" })); + assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.org" })); + assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.net" })); + }); + + await t.test("Test evaluateExpressionGroup with nested group", async (t) => { + const group = new ConditionExpressionGroup([ + new ConditionExpression("record", "contains", "mx1."), + new ConditionExpressionGroup([ + new ConditionExpression("record", "contains", "example.com"), + new ConditionExpression("record", "contains", "example.org", LOGICAL.OR), + ]), + ]); + assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." })); + assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" })); + assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.org" })); + assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" })); + assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.org" })); + assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1.example.net" })); + }); }); diff --git a/test/backend-test/monitor-conditions/test-operators.js b/test/backend-test/monitor-conditions/test-operators.js index e663c9a50..be18ee871 100644 --- a/test/backend-test/monitor-conditions/test-operators.js +++ b/test/backend-test/monitor-conditions/test-operators.js @@ -2,107 +2,109 @@ const test = require("node:test"); const assert = require("node:assert"); const { operatorMap, OP_CONTAINS, OP_NOT_CONTAINS, OP_LT, OP_GT, OP_LTE, OP_GTE, OP_STR_EQUALS, OP_STR_NOT_EQUALS, OP_NUM_EQUALS, OP_NUM_NOT_EQUALS, OP_STARTS_WITH, OP_ENDS_WITH, OP_NOT_STARTS_WITH, OP_NOT_ENDS_WITH } = require("../../../server/monitor-conditions/operators.js"); -test("Test StringEqualsOperator", async (t) => { - const op = operatorMap.get(OP_STR_EQUALS); - assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.com")); - assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.org")); - assert.strictEqual(false, op.test("1", 1)); // strict equality -}); +test("expressionOperators", async (t) => { + await t.test("Test StringEqualsOperator", async (t) => { + const op = operatorMap.get(OP_STR_EQUALS); + assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.com")); + assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.org")); + assert.strictEqual(false, op.test("1", 1)); // strict equality + }); -test("Test StringNotEqualsOperator", async (t) => { - const op = operatorMap.get(OP_STR_NOT_EQUALS); - assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.org")); - assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.com")); - assert.strictEqual(true, op.test(1, "1")); // variable is not typecasted (strict equality) -}); + await t.test("Test StringNotEqualsOperator", async (t) => { + const op = operatorMap.get(OP_STR_NOT_EQUALS); + assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.org")); + assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.com")); + assert.strictEqual(true, op.test(1, "1")); // variable is not typecasted (strict equality) + }); -test("Test ContainsOperator with scalar", async (t) => { - const op = operatorMap.get(OP_CONTAINS); - assert.strictEqual(true, op.test("mx1.example.org", "example.org")); - assert.strictEqual(false, op.test("mx1.example.org", "example.com")); -}); + await t.test("Test ContainsOperator with scalar", async (t) => { + const op = operatorMap.get(OP_CONTAINS); + assert.strictEqual(true, op.test("mx1.example.org", "example.org")); + assert.strictEqual(false, op.test("mx1.example.org", "example.com")); + }); -test("Test ContainsOperator with array", async (t) => { - const op = operatorMap.get(OP_CONTAINS); - assert.strictEqual(true, op.test([ "example.org" ], "example.org")); - assert.strictEqual(false, op.test([ "example.org" ], "example.com")); -}); + await t.test("Test ContainsOperator with array", async (t) => { + const op = operatorMap.get(OP_CONTAINS); + assert.strictEqual(true, op.test([ "example.org" ], "example.org")); + assert.strictEqual(false, op.test([ "example.org" ], "example.com")); + }); -test("Test NotContainsOperator with scalar", async (t) => { - const op = operatorMap.get(OP_NOT_CONTAINS); - assert.strictEqual(true, op.test("example.org", ".com")); - assert.strictEqual(false, op.test("example.org", ".org")); -}); + await t.test("Test NotContainsOperator with scalar", async (t) => { + const op = operatorMap.get(OP_NOT_CONTAINS); + assert.strictEqual(true, op.test("example.org", ".com")); + assert.strictEqual(false, op.test("example.org", ".org")); + }); -test("Test NotContainsOperator with array", async (t) => { - const op = operatorMap.get(OP_NOT_CONTAINS); - assert.strictEqual(true, op.test([ "example.org" ], "example.com")); - assert.strictEqual(false, op.test([ "example.org" ], "example.org")); -}); + await t.test("Test NotContainsOperator with array", async (t) => { + const op = operatorMap.get(OP_NOT_CONTAINS); + assert.strictEqual(true, op.test([ "example.org" ], "example.com")); + assert.strictEqual(false, op.test([ "example.org" ], "example.org")); + }); -test("Test StartsWithOperator", async (t) => { - const op = operatorMap.get(OP_STARTS_WITH); - assert.strictEqual(true, op.test("mx1.example.com", "mx1")); - assert.strictEqual(false, op.test("mx1.example.com", "mx2")); -}); + await t.test("Test StartsWithOperator", async (t) => { + const op = operatorMap.get(OP_STARTS_WITH); + assert.strictEqual(true, op.test("mx1.example.com", "mx1")); + assert.strictEqual(false, op.test("mx1.example.com", "mx2")); + }); -test("Test NotStartsWithOperator", async (t) => { - const op = operatorMap.get(OP_NOT_STARTS_WITH); - assert.strictEqual(true, op.test("mx1.example.com", "mx2")); - assert.strictEqual(false, op.test("mx1.example.com", "mx1")); -}); + await t.test("Test NotStartsWithOperator", async (t) => { + const op = operatorMap.get(OP_NOT_STARTS_WITH); + assert.strictEqual(true, op.test("mx1.example.com", "mx2")); + assert.strictEqual(false, op.test("mx1.example.com", "mx1")); + }); -test("Test EndsWithOperator", async (t) => { - const op = operatorMap.get(OP_ENDS_WITH); - assert.strictEqual(true, op.test("mx1.example.com", "example.com")); - assert.strictEqual(false, op.test("mx1.example.com", "example.net")); -}); + await t.test("Test EndsWithOperator", async (t) => { + const op = operatorMap.get(OP_ENDS_WITH); + assert.strictEqual(true, op.test("mx1.example.com", "example.com")); + assert.strictEqual(false, op.test("mx1.example.com", "example.net")); + }); -test("Test NotEndsWithOperator", async (t) => { - const op = operatorMap.get(OP_NOT_ENDS_WITH); - assert.strictEqual(true, op.test("mx1.example.com", "example.net")); - assert.strictEqual(false, op.test("mx1.example.com", "example.com")); -}); + await t.test("Test NotEndsWithOperator", async (t) => { + const op = operatorMap.get(OP_NOT_ENDS_WITH); + assert.strictEqual(true, op.test("mx1.example.com", "example.net")); + assert.strictEqual(false, op.test("mx1.example.com", "example.com")); + }); -test("Test NumberEqualsOperator", async (t) => { - const op = operatorMap.get(OP_NUM_EQUALS); - assert.strictEqual(true, op.test(1, 1)); - assert.strictEqual(true, op.test(1, "1")); - assert.strictEqual(false, op.test(1, "2")); -}); + await t.test("Test NumberEqualsOperator", async (t) => { + const op = operatorMap.get(OP_NUM_EQUALS); + assert.strictEqual(true, op.test(1, 1)); + assert.strictEqual(true, op.test(1, "1")); + assert.strictEqual(false, op.test(1, "2")); + }); -test("Test NumberNotEqualsOperator", async (t) => { - const op = operatorMap.get(OP_NUM_NOT_EQUALS); - assert.strictEqual(true, op.test(1, "2")); - assert.strictEqual(false, op.test(1, "1")); -}); + await t.test("Test NumberNotEqualsOperator", async (t) => { + const op = operatorMap.get(OP_NUM_NOT_EQUALS); + assert.strictEqual(true, op.test(1, "2")); + assert.strictEqual(false, op.test(1, "1")); + }); -test("Test LessThanOperator", async (t) => { - const op = operatorMap.get(OP_LT); - assert.strictEqual(true, op.test(1, 2)); - assert.strictEqual(true, op.test(1, "2")); - assert.strictEqual(false, op.test(1, 1)); -}); + await t.test("Test LessThanOperator", async (t) => { + const op = operatorMap.get(OP_LT); + assert.strictEqual(true, op.test(1, 2)); + assert.strictEqual(true, op.test(1, "2")); + assert.strictEqual(false, op.test(1, 1)); + }); -test("Test GreaterThanOperator", async (t) => { - const op = operatorMap.get(OP_GT); - assert.strictEqual(true, op.test(2, 1)); - assert.strictEqual(true, op.test(2, "1")); - assert.strictEqual(false, op.test(1, 1)); -}); + await t.test("Test GreaterThanOperator", async (t) => { + const op = operatorMap.get(OP_GT); + assert.strictEqual(true, op.test(2, 1)); + assert.strictEqual(true, op.test(2, "1")); + assert.strictEqual(false, op.test(1, 1)); + }); -test("Test LessThanOrEqualToOperator", async (t) => { - const op = operatorMap.get(OP_LTE); - assert.strictEqual(true, op.test(1, 1)); - assert.strictEqual(true, op.test(1, 2)); - assert.strictEqual(true, op.test(1, "2")); - assert.strictEqual(false, op.test(1, 0)); -}); + await t.test("Test LessThanOrEqualToOperator", async (t) => { + const op = operatorMap.get(OP_LTE); + assert.strictEqual(true, op.test(1, 1)); + assert.strictEqual(true, op.test(1, 2)); + assert.strictEqual(true, op.test(1, "2")); + assert.strictEqual(false, op.test(1, 0)); + }); -test("Test GreaterThanOrEqualToOperator", async (t) => { - const op = operatorMap.get(OP_GTE); - assert.strictEqual(true, op.test(1, 1)); - assert.strictEqual(true, op.test(2, 1)); - assert.strictEqual(true, op.test(2, "2")); - assert.strictEqual(false, op.test(2, 3)); + await t.test("Test GreaterThanOrEqualToOperator", async (t) => { + const op = operatorMap.get(OP_GTE); + assert.strictEqual(true, op.test(1, 1)); + assert.strictEqual(true, op.test(2, 1)); + assert.strictEqual(true, op.test(2, "2")); + assert.strictEqual(false, op.test(2, 3)); + }); }); diff --git a/test/backend-test/snapshots/og-images/all-down.svg b/test/backend-test/snapshots/og-images/all-down.svg new file mode 100644 index 000000000..ed9c8533a --- /dev/null +++ b/test/backend-test/snapshots/og-images/all-down.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + Infrastructure + + + + + + + + Degraded Service + + + + + + 5 Up + + + + 1 Pending + + POWERED BY UPTIME KUMA + UPDATED AT 2024-01-01T00:00:00.000Z + \ No newline at end of file diff --git a/test/backend-test/snapshots/og-images/all-up-custom-icon-no-branding.svg b/test/backend-test/snapshots/og-images/all-up-custom-icon-no-branding.svg new file mode 100644 index 000000000..068ad7cfb --- /dev/null +++ b/test/backend-test/snapshots/og-images/all-up-custom-icon-no-branding.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + Production + + + + + + + + All Systems Operational + + + + + + 6 Up + + + + 2 Pending + + UPDATED AT 2024-01-01T00:00:00.000Z + \ No newline at end of file diff --git a/test/backend-test/snapshots/og-images/all-up-custom-icon-with-branding.svg b/test/backend-test/snapshots/og-images/all-up-custom-icon-with-branding.svg new file mode 100644 index 000000000..2accd549c --- /dev/null +++ b/test/backend-test/snapshots/og-images/all-up-custom-icon-with-branding.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + Production + + + + + + + + All Systems Operational + + + + + + 6 Up + + + + 2 Pending + + POWERED BY UPTIME KUMA + UPDATED AT 2024-01-01T00:00:00.000Z + \ No newline at end of file diff --git a/test/backend-test/snapshots/og-images/all-up-long-title.svg b/test/backend-test/snapshots/og-images/all-up-long-title.svg new file mode 100644 index 000000000..e23cbe514 --- /dev/null +++ b/test/backend-test/snapshots/og-images/all-up-long-title.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + This is an extremely long status page... + + + + + + + + All Systems Operational + + + + + + 4 Up + + + + 1 Pending + + POWERED BY UPTIME KUMA + UPDATED AT 2024-01-01T00:00:00.000Z + \ No newline at end of file diff --git a/test/backend-test/snapshots/og-images/all-up-many-monitors.svg b/test/backend-test/snapshots/og-images/all-up-many-monitors.svg new file mode 100644 index 000000000..6f9a4ebce --- /dev/null +++ b/test/backend-test/snapshots/og-images/all-up-many-monitors.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + Enterprise + + + + + + + + All Systems Operational + + + + + + 170 Up + + + + 20 Down + + + + 6 Maintenance + + + + 4 Pending + + POWERED BY UPTIME KUMA + UPDATED AT 2024-01-01T00:00:00.000Z + \ No newline at end of file diff --git a/test/backend-test/snapshots/og-images/all-up-no-branding.svg b/test/backend-test/snapshots/og-images/all-up-no-branding.svg new file mode 100644 index 000000000..835b07444 --- /dev/null +++ b/test/backend-test/snapshots/og-images/all-up-no-branding.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + Production + + + + + + + + All Systems Operational + + + + + + 6 Up + + + + 2 Pending + + UPDATED AT 2024-01-01T00:00:00.000Z + \ No newline at end of file diff --git a/test/backend-test/snapshots/og-images/all-up.svg b/test/backend-test/snapshots/og-images/all-up.svg new file mode 100644 index 000000000..257679f05 --- /dev/null +++ b/test/backend-test/snapshots/og-images/all-up.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + Production + + + + + + + + All Systems Operational + + + + + + 6 Up + + + + 2 Pending + + POWERED BY UPTIME KUMA + UPDATED AT 2024-01-01T00:00:00.000Z + \ No newline at end of file diff --git a/test/backend-test/snapshots/og-images/maintenance.svg b/test/backend-test/snapshots/og-images/maintenance.svg new file mode 100644 index 000000000..005431835 --- /dev/null +++ b/test/backend-test/snapshots/og-images/maintenance.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + Maintenance + + + + + + + + Under maintenance + + + + + 3 Monitors tracked + + POWERED BY UPTIME KUMA + UPDATED AT 2024-01-01T00:00:00.000Z + \ No newline at end of file diff --git a/test/backend-test/snapshots/og-images/partial.svg b/test/backend-test/snapshots/og-images/partial.svg new file mode 100644 index 000000000..8c11a743c --- /dev/null +++ b/test/backend-test/snapshots/og-images/partial.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + API Services + + + + + + + + Partially Degraded Service + + + + + + 10 Up + + + + 1 Down + + + + 1 Pending + + POWERED BY UPTIME KUMA + UPDATED AT 2024-01-01T00:00:00.000Z + \ No newline at end of file diff --git a/test/backend-test/test-status-page-og-image.js b/test/backend-test/test-status-page-og-image.js new file mode 100644 index 000000000..fa6c86e15 --- /dev/null +++ b/test/backend-test/test-status-page-og-image.js @@ -0,0 +1,401 @@ +const test = require("node:test"); +const assert = require("node:assert"); +const sharp = require("sharp"); +const StatusPage = require("../../server/model/status_page"); +const { generateOGImageSVG, getStatusColor, escapeXml } = require("../../server/utils/og-image"); +const fs = require("fs"); +const path = require("path"); +const { + STATUS_PAGE_ALL_UP, + STATUS_PAGE_PARTIAL_DOWN, + STATUS_PAGE_ALL_DOWN, + MAINTENANCE +} = require("../../src/util"); + +const SNAPSHOTS_DIR = path.join(__dirname, "snapshots", "og-images"); + +// Ensure snapshots directory exists +if (!fs.existsSync(SNAPSHOTS_DIR)) { + fs.mkdirSync(SNAPSHOTS_DIR, { recursive: true }); +} + +// Fixed timestamp for consistent snapshots (2024-01-01 00:00:00 UTC) +const FIXED_TIMESTAMP = 1704067200000; + +// ============================================================================ +// Test Fixtures +// ============================================================================ + +const TEST_SCENARIOS = [ + { + name: "all-up", + title: "Production", + statusCode: STATUS_PAGE_ALL_UP, + monitorCount: 8 + }, + { + name: "all-down", + title: "Infrastructure", + statusCode: STATUS_PAGE_ALL_DOWN, + monitorCount: 6 + }, + { + name: "maintenance", + title: "Maintenance", + statusCode: MAINTENANCE, + monitorCount: 3 + }, + { + name: "partial", + title: "API Services", + statusCode: STATUS_PAGE_PARTIAL_DOWN, + monitorCount: 12 + }, + { + name: "all-up-many-monitors", + title: "Enterprise", + statusCode: STATUS_PAGE_ALL_UP, + monitorCount: 200 + }, + { + name: "all-up-no-branding", + title: "Production", + statusCode: STATUS_PAGE_ALL_UP, + monitorCount: 8, + showPoweredBy: false + }, + { + name: "all-up-custom-icon-no-branding", + title: "Production", + statusCode: STATUS_PAGE_ALL_UP, + monitorCount: 8, + icon: "/icon.svg", + showPoweredBy: false + }, + { + name: "all-up-custom-icon-with-branding", + title: "Production", + statusCode: STATUS_PAGE_ALL_UP, + monitorCount: 8, + icon: "/icon.svg", + showPoweredBy: true + }, + { + name: "all-up-long-title", + title: "This is an extremely long status page title that will need to be truncated", + statusCode: STATUS_PAGE_ALL_UP, + monitorCount: 5 + } +]; + +// ============================================================================ +// Test Helpers +// ============================================================================ + +/** + * Compare SVG with snapshot, create if doesn't exist + * @param {string} svg SVG content + * @param {string} snapshotName Snapshot filename + * @returns {void} + */ +function assertMatchesSnapshot(svg, snapshotName) { + const snapshotPath = path.join(SNAPSHOTS_DIR, snapshotName); + + if (fs.existsSync(snapshotPath)) { + const snapshot = fs.readFileSync(snapshotPath, "utf8"); + assert.strictEqual(svg, snapshot, `SVG should match snapshot: ${snapshotName}`); + } else { + fs.writeFileSync(snapshotPath, svg, "utf8"); + console.log(`Created snapshot: ${snapshotName}`); + } +} + +/** + * Creates a mock status page with RSS data + * @param {object} overrides Override default values + * @param {object} rssData RSS data to return + * @returns {object} Mock status page and cleanup function + */ +function createMockStatusPage(overrides = {}, rssData = null) { + const mockPage = { + id: 1, + slug: "test", + title: "Test Page", + description: "Test Description", + ...overrides + }; + + const originalGetRSSData = StatusPage.getRSSPageData; + + if (rssData) { + StatusPage.getRSSPageData = async () => rssData; + } + + const cleanup = () => { + StatusPage.getRSSPageData = originalGetRSSData; + }; + + return { mockPage, + cleanup }; +} + +/** + * Verify PNG buffer structure and metadata + * @param {Buffer} buffer PNG buffer to verify + * @returns {Promise} + */ +async function assertValidPNG(buffer) { + assert.ok(buffer instanceof Buffer, "Should return a Buffer"); + assert.ok(buffer.length > 0, "Buffer should not be empty"); + + // Check PNG signature (first 8 bytes) + const pngSignature = Buffer.from([ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ]); + const actualSignature = buffer.slice(0, 8); + assert.deepStrictEqual( + actualSignature, + pngSignature, + "Should have valid PNG signature" + ); + + // Use sharp to verify metadata + const metadata = await sharp(buffer).metadata(); + assert.strictEqual(metadata.format, "png", "Should be PNG format"); + assert.strictEqual(metadata.width, 1200, "Should have width 1200"); + assert.strictEqual(metadata.height, 630, "Should have height 630"); +} + +// ============================================================================ +// Unit Tests - Helper Functions +// ============================================================================ + +test("escapeXml()", async (t) => { + + await t.test("should escape all XML special characters", () => { + assert.strictEqual(escapeXml("&"), "&"); + assert.strictEqual(escapeXml("<"), "<"); + assert.strictEqual(escapeXml(">"), ">"); + assert.strictEqual(escapeXml("\""), """); + assert.strictEqual(escapeXml("'"), "'"); + }); + + await t.test("should handle mixed special characters", () => { + const input = "Company & Services \"Test\" 'Quote'"; + const expected = "Company & Services <Status> "Test" 'Quote'"; + assert.strictEqual(escapeXml(input), expected); + }); +}); + +test("getStatusColor()", async (t) => { + + await t.test("should return green for all systems up", () => { + assert.strictEqual(getStatusColor(STATUS_PAGE_ALL_UP), "#10b981"); + }); + + await t.test("should return yellow for partial degradation", () => { + assert.strictEqual(getStatusColor(STATUS_PAGE_PARTIAL_DOWN), "#f59e0b"); + }); + + await t.test("should return red for all systems down", () => { + assert.strictEqual(getStatusColor(STATUS_PAGE_ALL_DOWN), "#ef4444"); + }); + + await t.test("should return blue for maintenance", () => { + assert.strictEqual(getStatusColor(MAINTENANCE), "#3b82f6"); + }); + + await t.test("should return gray for no services", () => { + assert.strictEqual(getStatusColor(-1), "#6b7280"); + }); +}); + +test("StatusPage.overallStatus()", async (t) => { + + await t.test("should return -1 for empty heartbeats", () => { + const status = StatusPage.overallStatus([]); + assert.strictEqual(status, -1); + }); + + await t.test("should return ALL_UP when all monitors are up", () => { + const status = StatusPage.overallStatus([{ status: 1 }, { status: 1 }]); + assert.strictEqual(status, STATUS_PAGE_ALL_UP); + }); + + await t.test("should return PARTIAL_DOWN when monitors are mixed", () => { + const status = StatusPage.overallStatus([{ status: 1 }, { status: 0 }]); + assert.strictEqual(status, STATUS_PAGE_PARTIAL_DOWN); + }); + + await t.test("should return ALL_DOWN when all monitors are down", () => { + const status = StatusPage.overallStatus([{ status: 0 }, { status: 0 }]); + assert.strictEqual(status, STATUS_PAGE_ALL_DOWN); + }); +}); + +test("StatusPage.getStatusDescription()", async (t) => { + + await t.test("should return description for no services", () => { + assert.strictEqual(StatusPage.getStatusDescription(-1), "No Services"); + }); + + await t.test("should return description for all systems operational", () => { + assert.strictEqual(StatusPage.getStatusDescription(STATUS_PAGE_ALL_UP), "All Systems Operational"); + }); + + await t.test("should return description for partially degraded", () => { + assert.strictEqual(StatusPage.getStatusDescription(STATUS_PAGE_PARTIAL_DOWN), "Partially Degraded Service"); + }); + + await t.test("should return description for degraded service", () => { + assert.strictEqual(StatusPage.getStatusDescription(STATUS_PAGE_ALL_DOWN), "Degraded Service"); + }); + + await t.test("should return description for maintenance", () => { + assert.strictEqual(StatusPage.getStatusDescription(MAINTENANCE), "Under maintenance"); + }); +}); + +// ============================================================================ +// Basic Structure Tests +// ============================================================================ + +test("generateOGImageSVG() - basic structure", async (t) => { + + await t.test("should generate valid SVG with required elements", () => { + const svg = generateOGImageSVG( + "Test", + "All OK", + "#10b981", + null, + true, + FIXED_TIMESTAMP, + Array(5).fill({}) + ); + + assert.ok(svg.startsWith(""), "Should have closing svg tag"); + }); +}); + +// ============================================================================ +// Snapshot Tests - Comprehensive Scenarios +// ============================================================================ + +test("generateOGImageSVG() snapshots - all scenarios", async (t) => { + + await t.test("all test scenarios", () => { + TEST_SCENARIOS.forEach((scenario) => { + const statusDescription = StatusPage.getStatusDescription(scenario.statusCode); + const statusColor = getStatusColor(scenario.statusCode); + const icon = scenario.icon || null; + const showPoweredBy = scenario.showPoweredBy !== undefined ? scenario.showPoweredBy : true; + + // For scenarios with many monitors (>3), show status count summary + // Otherwise show individual monitors or total count + let monitors; + if (scenario.monitorCount > 3) { + // Create realistic monitor mix for status summary + monitors = []; + const upCount = Math.floor(scenario.monitorCount * 0.85); // 85% up + const downCount = Math.floor(scenario.monitorCount * 0.10); // 10% down + const maintenanceCount = Math.floor(scenario.monitorCount * 0.03); // 3% maintenance + const pendingCount = scenario.monitorCount - upCount - downCount - maintenanceCount; // Rest pending + + for (let i = 0; i < upCount; i++) { + monitors.push({ name: `Monitor ${i + 1}`, + status: 1 }); + } + for (let i = 0; i < downCount; i++) { + monitors.push({ name: `Monitor ${upCount + i + 1}`, + status: 0 }); + } + for (let i = 0; i < maintenanceCount; i++) { + monitors.push({ name: `Monitor ${upCount + downCount + i + 1}`, + status: 3 }); + } + for (let i = 0; i < pendingCount; i++) { + monitors.push({ name: `Monitor ${upCount + downCount + maintenanceCount + i + 1}`, + status: 2 }); + } + } else { + monitors = Array(scenario.monitorCount).fill({}); // Show count + } + + const svg = generateOGImageSVG( + scenario.title, + statusDescription, + statusColor, + icon, + showPoweredBy, + FIXED_TIMESTAMP, + monitors + ); + assertMatchesSnapshot(svg, `${scenario.name}.svg`); + }); + }); +}); + +// ============================================================================ +// Integration Tests - PNG Conversion +// ============================================================================ + +test("StatusPage.generateOGImage() - PNG conversion", async (t) => { + + await t.test("should convert SVG to valid PNG", async () => { + const { mockPage, cleanup } = createMockStatusPage( + { title: "Test Page" }, + { + heartbeats: [{ status: 1 }], + statusDescription: "All Systems Operational" + } + ); + + try { + const buffer = await StatusPage.generateOGImage(mockPage); + await assertValidPNG(buffer); + } finally { + cleanup(); + } + }); + + await t.test("should generate PNG with reasonable file size", async () => { + const { mockPage, cleanup } = createMockStatusPage( + {}, + { + heartbeats: [{ status: 1 }], + statusDescription: "All Systems Operational" + } + ); + + try { + const buffer = await StatusPage.generateOGImage(mockPage); + assert.ok(buffer.length < 100 * 1024, "PNG should be less than 100KB"); + assert.ok(buffer.length > 1000, "PNG should be more than 1KB"); + } finally { + cleanup(); + } + }); + + await t.test("should handle different status page configurations", async () => { + const { mockPage, cleanup } = createMockStatusPage( + { + title: "Production", + show_powered_by: false, + icon: "/icon.svg" + }, + { + heartbeats: [{ status: 1 }, { status: 0 }], + statusDescription: "Partially Degraded Service" + } + ); + + try { + const buffer = await StatusPage.generateOGImage(mockPage); + await assertValidPNG(buffer); + } finally { + cleanup(); + } + }); +}); diff --git a/test/e2e/specs/status-page.spec.js b/test/e2e/specs/status-page.spec.js index 1964f92b5..d5807217f 100644 --- a/test/e2e/specs/status-page.spec.js +++ b/test/e2e/specs/status-page.spec.js @@ -156,6 +156,43 @@ test.describe("Status Page", () => { await screenshot(testInfo, page); }); + test("OG image and meta tags", async ({ page, request }) => { + test.setTimeout(60000); + + await page.goto("./dashboard"); + await login(page); + + // Navigate to default status page + await page.goto("./status/default"); + await page.waitForTimeout(1000); + + // Verify OG meta tags exist + const ogTitle = await page.locator("meta[property='og:title']").getAttribute("content"); + expect(ogTitle).toBeTruthy(); + + const ogType = await page.locator("meta[property='og:type']").getAttribute("content"); + expect(ogType).toBe("website"); + + const twitterCard = await page.locator("meta[name='twitter:card']").getAttribute("content"); + expect(twitterCard).toBe("summary_large_image"); + + // Test OG image API endpoint + const imageResponse = await request.get("/api/status-page/default/image"); + expect(imageResponse.status()).toBe(200); + expect(imageResponse.headers()["content-type"]).toBe("image/png"); + + const buffer = await imageResponse.body(); + expect(buffer.length).toBeGreaterThan(0); + + // Verify PNG signature + const pngSignature = Buffer.from([ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ]); + expect(buffer.subarray(0, 8)).toEqual(pngSignature); + + // Test 404 for non-existent page + const notFoundResponse = await request.get("/api/status-page/does-not-exist/image"); + expect(notFoundResponse.status()).toBe(404); + }); + // @todo Test certificate expiry // @todo Test domain names From 0cb0fab0945f0c1eedfd55a0e7e36b969e3a9b5a Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 17:32:40 +0100 Subject: [PATCH 2/5] use test suites for og images as well --- .../backend-test/test-status-page-og-image.js | 187 ++++++++---------- 1 file changed, 79 insertions(+), 108 deletions(-) diff --git a/test/backend-test/test-status-page-og-image.js b/test/backend-test/test-status-page-og-image.js index fa6c86e15..bf2b1c064 100644 --- a/test/backend-test/test-status-page-og-image.js +++ b/test/backend-test/test-status-page-og-image.js @@ -1,4 +1,4 @@ -const test = require("node:test"); +const { describe, test } = require("node:test"); const assert = require("node:assert"); const sharp = require("sharp"); const StatusPage = require("../../server/model/status_page"); @@ -22,10 +22,6 @@ if (!fs.existsSync(SNAPSHOTS_DIR)) { // Fixed timestamp for consistent snapshots (2024-01-01 00:00:00 UTC) const FIXED_TIMESTAMP = 1704067200000; -// ============================================================================ -// Test Fixtures -// ============================================================================ - const TEST_SCENARIOS = [ { name: "all-up", @@ -88,10 +84,6 @@ const TEST_SCENARIOS = [ } ]; -// ============================================================================ -// Test Helpers -// ============================================================================ - /** * Compare SVG with snapshot, create if doesn't exist * @param {string} svg SVG content @@ -164,103 +156,94 @@ async function assertValidPNG(buffer) { assert.strictEqual(metadata.height, 630, "Should have height 630"); } -// ============================================================================ -// Unit Tests - Helper Functions -// ============================================================================ +describe("OG Image Helper Functions", () => { + describe("escapeXml()", () => { + test("escapes all XML special characters", () => { + assert.strictEqual(escapeXml("&"), "&"); + assert.strictEqual(escapeXml("<"), "<"); + assert.strictEqual(escapeXml(">"), ">"); + assert.strictEqual(escapeXml("\""), """); + assert.strictEqual(escapeXml("'"), "'"); + }); -test("escapeXml()", async (t) => { - - await t.test("should escape all XML special characters", () => { - assert.strictEqual(escapeXml("&"), "&"); - assert.strictEqual(escapeXml("<"), "<"); - assert.strictEqual(escapeXml(">"), ">"); - assert.strictEqual(escapeXml("\""), """); - assert.strictEqual(escapeXml("'"), "'"); + test("handles mixed special characters", () => { + const input = "Company & Services \"Test\" 'Quote'"; + const expected = "Company & Services <Status> "Test" 'Quote'"; + assert.strictEqual(escapeXml(input), expected); + }); }); - await t.test("should handle mixed special characters", () => { - const input = "Company & Services \"Test\" 'Quote'"; - const expected = "Company & Services <Status> "Test" 'Quote'"; - assert.strictEqual(escapeXml(input), expected); + describe("getStatusColor()", () => { + test("returns green for all systems up", () => { + assert.strictEqual(getStatusColor(STATUS_PAGE_ALL_UP), "#10b981"); + }); + + test("returns yellow for partial degradation", () => { + assert.strictEqual(getStatusColor(STATUS_PAGE_PARTIAL_DOWN), "#f59e0b"); + }); + + test("returns red for all systems down", () => { + assert.strictEqual(getStatusColor(STATUS_PAGE_ALL_DOWN), "#ef4444"); + }); + + test("returns blue for maintenance", () => { + assert.strictEqual(getStatusColor(MAINTENANCE), "#3b82f6"); + }); + + test("returns gray for no services", () => { + assert.strictEqual(getStatusColor(-1), "#6b7280"); + }); }); }); -test("getStatusColor()", async (t) => { +describe("StatusPage Model", () => { + describe("overallStatus()", () => { + test("returns -1 for empty heartbeats", () => { + const status = StatusPage.overallStatus([]); + assert.strictEqual(status, -1); + }); - await t.test("should return green for all systems up", () => { - assert.strictEqual(getStatusColor(STATUS_PAGE_ALL_UP), "#10b981"); + test("returns ALL_UP when all monitors are up", () => { + const status = StatusPage.overallStatus([{ status: 1 }, { status: 1 }]); + assert.strictEqual(status, STATUS_PAGE_ALL_UP); + }); + + test("returns PARTIAL_DOWN when monitors are mixed", () => { + const status = StatusPage.overallStatus([{ status: 1 }, { status: 0 }]); + assert.strictEqual(status, STATUS_PAGE_PARTIAL_DOWN); + }); + + test("returns ALL_DOWN when all monitors are down", () => { + const status = StatusPage.overallStatus([{ status: 0 }, { status: 0 }]); + assert.strictEqual(status, STATUS_PAGE_ALL_DOWN); + }); }); - await t.test("should return yellow for partial degradation", () => { - assert.strictEqual(getStatusColor(STATUS_PAGE_PARTIAL_DOWN), "#f59e0b"); - }); + describe("getStatusDescription()", () => { + test("returns description for no services", () => { + assert.strictEqual(StatusPage.getStatusDescription(-1), "No Services"); + }); - await t.test("should return red for all systems down", () => { - assert.strictEqual(getStatusColor(STATUS_PAGE_ALL_DOWN), "#ef4444"); - }); + test("returns description for all systems operational", () => { + assert.strictEqual(StatusPage.getStatusDescription(STATUS_PAGE_ALL_UP), "All Systems Operational"); + }); - await t.test("should return blue for maintenance", () => { - assert.strictEqual(getStatusColor(MAINTENANCE), "#3b82f6"); - }); + test("returns description for partially degraded", () => { + assert.strictEqual(StatusPage.getStatusDescription(STATUS_PAGE_PARTIAL_DOWN), "Partially Degraded Service"); + }); - await t.test("should return gray for no services", () => { - assert.strictEqual(getStatusColor(-1), "#6b7280"); + test("returns description for degraded service", () => { + assert.strictEqual(StatusPage.getStatusDescription(STATUS_PAGE_ALL_DOWN), "Degraded Service"); + }); + + test("returns description for maintenance", () => { + assert.strictEqual(StatusPage.getStatusDescription(MAINTENANCE), "Under maintenance"); + }); }); }); -test("StatusPage.overallStatus()", async (t) => { - - await t.test("should return -1 for empty heartbeats", () => { - const status = StatusPage.overallStatus([]); - assert.strictEqual(status, -1); - }); - - await t.test("should return ALL_UP when all monitors are up", () => { - const status = StatusPage.overallStatus([{ status: 1 }, { status: 1 }]); - assert.strictEqual(status, STATUS_PAGE_ALL_UP); - }); - - await t.test("should return PARTIAL_DOWN when monitors are mixed", () => { - const status = StatusPage.overallStatus([{ status: 1 }, { status: 0 }]); - assert.strictEqual(status, STATUS_PAGE_PARTIAL_DOWN); - }); - - await t.test("should return ALL_DOWN when all monitors are down", () => { - const status = StatusPage.overallStatus([{ status: 0 }, { status: 0 }]); - assert.strictEqual(status, STATUS_PAGE_ALL_DOWN); - }); -}); - -test("StatusPage.getStatusDescription()", async (t) => { - - await t.test("should return description for no services", () => { - assert.strictEqual(StatusPage.getStatusDescription(-1), "No Services"); - }); - - await t.test("should return description for all systems operational", () => { - assert.strictEqual(StatusPage.getStatusDescription(STATUS_PAGE_ALL_UP), "All Systems Operational"); - }); - - await t.test("should return description for partially degraded", () => { - assert.strictEqual(StatusPage.getStatusDescription(STATUS_PAGE_PARTIAL_DOWN), "Partially Degraded Service"); - }); - - await t.test("should return description for degraded service", () => { - assert.strictEqual(StatusPage.getStatusDescription(STATUS_PAGE_ALL_DOWN), "Degraded Service"); - }); - - await t.test("should return description for maintenance", () => { - assert.strictEqual(StatusPage.getStatusDescription(MAINTENANCE), "Under maintenance"); - }); -}); - -// ============================================================================ -// Basic Structure Tests -// ============================================================================ - -test("generateOGImageSVG() - basic structure", async (t) => { - - await t.test("should generate valid SVG with required elements", () => { +describe("generateOGImageSVG()", () => { + test("generates valid SVG with required elements", () => { const svg = generateOGImageSVG( "Test", "All OK", @@ -277,15 +260,8 @@ test("generateOGImageSVG() - basic structure", async (t) => { assert.ok(svg.includes("height=\"630\""), "Should have height 630"); assert.ok(svg.includes(""), "Should have closing svg tag"); }); -}); -// ============================================================================ -// Snapshot Tests - Comprehensive Scenarios -// ============================================================================ - -test("generateOGImageSVG() snapshots - all scenarios", async (t) => { - - await t.test("all test scenarios", () => { + test("generates snapshots for all test scenarios", () => { TEST_SCENARIOS.forEach((scenario) => { const statusDescription = StatusPage.getStatusDescription(scenario.statusCode); const statusColor = getStatusColor(scenario.statusCode); @@ -337,13 +313,8 @@ test("generateOGImageSVG() snapshots - all scenarios", async (t) => { }); }); -// ============================================================================ -// Integration Tests - PNG Conversion -// ============================================================================ - -test("StatusPage.generateOGImage() - PNG conversion", async (t) => { - - await t.test("should convert SVG to valid PNG", async () => { +describe("StatusPage.generateOGImage()", () => { + test("converts SVG to valid PNG", async () => { const { mockPage, cleanup } = createMockStatusPage( { title: "Test Page" }, { @@ -360,7 +331,7 @@ test("StatusPage.generateOGImage() - PNG conversion", async (t) => { } }); - await t.test("should generate PNG with reasonable file size", async () => { + test("generates PNG with reasonable file size", async () => { const { mockPage, cleanup } = createMockStatusPage( {}, { @@ -378,7 +349,7 @@ test("StatusPage.generateOGImage() - PNG conversion", async (t) => { } }); - await t.test("should handle different status page configurations", async () => { + test("handles different status page configurations", async () => { const { mockPage, cleanup } = createMockStatusPage( { title: "Production", From 6e0c62892c72e5c3c75eaa78d703843f7954a82d Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 17:40:42 +0100 Subject: [PATCH 3/5] rework testcase naming --- .../backend-test/test-status-page-og-image.js | 279 ++++++++++-------- 1 file changed, 163 insertions(+), 116 deletions(-) diff --git a/test/backend-test/test-status-page-og-image.js b/test/backend-test/test-status-page-og-image.js index bf2b1c064..68bafee5e 100644 --- a/test/backend-test/test-status-page-og-image.js +++ b/test/backend-test/test-status-page-og-image.js @@ -95,7 +95,7 @@ function assertMatchesSnapshot(svg, snapshotName) { if (fs.existsSync(snapshotPath)) { const snapshot = fs.readFileSync(snapshotPath, "utf8"); - assert.strictEqual(svg, snapshot, `SVG should match snapshot: ${snapshotName}`); + assert.strictEqual(svg, snapshot, `Expected SVG to match snapshot: ${snapshotName}`); } else { fs.writeFileSync(snapshotPath, svg, "utf8"); console.log(`Created snapshot: ${snapshotName}`); @@ -137,8 +137,8 @@ function createMockStatusPage(overrides = {}, rssData = null) { * @returns {Promise} */ async function assertValidPNG(buffer) { - assert.ok(buffer instanceof Buffer, "Should return a Buffer"); - assert.ok(buffer.length > 0, "Buffer should not be empty"); + assert.ok(buffer instanceof Buffer, "Expected Buffer instance"); + assert.ok(buffer.length > 0, "Expected non-empty buffer"); // Check PNG signature (first 8 bytes) const pngSignature = Buffer.from([ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ]); @@ -146,27 +146,39 @@ async function assertValidPNG(buffer) { assert.deepStrictEqual( actualSignature, pngSignature, - "Should have valid PNG signature" + "Expected valid PNG signature" ); // Use sharp to verify metadata const metadata = await sharp(buffer).metadata(); - assert.strictEqual(metadata.format, "png", "Should be PNG format"); - assert.strictEqual(metadata.width, 1200, "Should have width 1200"); - assert.strictEqual(metadata.height, 630, "Should have height 630"); + assert.strictEqual(metadata.format, "png", "Expected PNG format"); + assert.strictEqual(metadata.width, 1200, "Expected width of 1200"); + assert.strictEqual(metadata.height, 630, "Expected height of 630"); } describe("OG Image Helper Functions", () => { describe("escapeXml()", () => { - test("escapes all XML special characters", () => { + test("escapeXml() replaces ampersand with entity", () => { assert.strictEqual(escapeXml("&"), "&"); + }); + + test("escapeXml() replaces less-than with entity", () => { assert.strictEqual(escapeXml("<"), "<"); + }); + + test("escapeXml() replaces greater-than with entity", () => { assert.strictEqual(escapeXml(">"), ">"); + }); + + test("escapeXml() replaces double quote with entity", () => { assert.strictEqual(escapeXml("\""), """); + }); + + test("escapeXml() replaces single quote with entity", () => { assert.strictEqual(escapeXml("'"), "'"); }); - test("handles mixed special characters", () => { + test("escapeXml() handles multiple special characters in one string", () => { const input = "Company & Services \"Test\" 'Quote'"; const expected = "Company & Services <Status> "Test" 'Quote'"; assert.strictEqual(escapeXml(input), expected); @@ -174,76 +186,30 @@ describe("OG Image Helper Functions", () => { }); describe("getStatusColor()", () => { - test("returns green for all systems up", () => { + test("getStatusColor() returns green when all systems are up", () => { assert.strictEqual(getStatusColor(STATUS_PAGE_ALL_UP), "#10b981"); }); - test("returns yellow for partial degradation", () => { + test("getStatusColor() returns yellow when service is partially degraded", () => { assert.strictEqual(getStatusColor(STATUS_PAGE_PARTIAL_DOWN), "#f59e0b"); }); - test("returns red for all systems down", () => { + test("getStatusColor() returns red when all systems are down", () => { assert.strictEqual(getStatusColor(STATUS_PAGE_ALL_DOWN), "#ef4444"); }); - test("returns blue for maintenance", () => { + test("getStatusColor() returns blue when under maintenance", () => { assert.strictEqual(getStatusColor(MAINTENANCE), "#3b82f6"); }); - test("returns gray for no services", () => { + test("getStatusColor() returns gray when there are no services", () => { assert.strictEqual(getStatusColor(-1), "#6b7280"); }); }); }); -describe("StatusPage Model", () => { - describe("overallStatus()", () => { - test("returns -1 for empty heartbeats", () => { - const status = StatusPage.overallStatus([]); - assert.strictEqual(status, -1); - }); - - test("returns ALL_UP when all monitors are up", () => { - const status = StatusPage.overallStatus([{ status: 1 }, { status: 1 }]); - assert.strictEqual(status, STATUS_PAGE_ALL_UP); - }); - - test("returns PARTIAL_DOWN when monitors are mixed", () => { - const status = StatusPage.overallStatus([{ status: 1 }, { status: 0 }]); - assert.strictEqual(status, STATUS_PAGE_PARTIAL_DOWN); - }); - - test("returns ALL_DOWN when all monitors are down", () => { - const status = StatusPage.overallStatus([{ status: 0 }, { status: 0 }]); - assert.strictEqual(status, STATUS_PAGE_ALL_DOWN); - }); - }); - - describe("getStatusDescription()", () => { - test("returns description for no services", () => { - assert.strictEqual(StatusPage.getStatusDescription(-1), "No Services"); - }); - - test("returns description for all systems operational", () => { - assert.strictEqual(StatusPage.getStatusDescription(STATUS_PAGE_ALL_UP), "All Systems Operational"); - }); - - test("returns description for partially degraded", () => { - assert.strictEqual(StatusPage.getStatusDescription(STATUS_PAGE_PARTIAL_DOWN), "Partially Degraded Service"); - }); - - test("returns description for degraded service", () => { - assert.strictEqual(StatusPage.getStatusDescription(STATUS_PAGE_ALL_DOWN), "Degraded Service"); - }); - - test("returns description for maintenance", () => { - assert.strictEqual(StatusPage.getStatusDescription(MAINTENANCE), "Under maintenance"); - }); - }); -}); - describe("generateOGImageSVG()", () => { - test("generates valid SVG with required elements", () => { + test("generateOGImageSVG() starts with XML declaration", () => { const svg = generateOGImageSVG( "Test", "All OK", @@ -254,14 +220,66 @@ describe("generateOGImageSVG()", () => { Array(5).fill({}) ); - assert.ok(svg.startsWith(""), "Should have closing svg tag"); + assert.ok(svg.startsWith(" { + test("generateOGImageSVG() includes SVG namespace", () => { + const svg = generateOGImageSVG( + "Test", + "All OK", + "#10b981", + null, + true, + FIXED_TIMESTAMP, + Array(5).fill({}) + ); + + assert.ok(svg.includes("xmlns=\"http://www.w3.org/2000/svg\""), "Expected SVG namespace"); + }); + + test("generateOGImageSVG() sets width to 1200", () => { + const svg = generateOGImageSVG( + "Test", + "All OK", + "#10b981", + null, + true, + FIXED_TIMESTAMP, + Array(5).fill({}) + ); + + assert.ok(svg.includes("width=\"1200\""), "Expected width of 1200"); + }); + + test("generateOGImageSVG() sets height to 630", () => { + const svg = generateOGImageSVG( + "Test", + "All OK", + "#10b981", + null, + true, + FIXED_TIMESTAMP, + Array(5).fill({}) + ); + + assert.ok(svg.includes("height=\"630\""), "Expected height of 630"); + }); + + test("generateOGImageSVG() includes closing SVG tag", () => { + const svg = generateOGImageSVG( + "Test", + "All OK", + "#10b981", + null, + true, + FIXED_TIMESTAMP, + Array(5).fill({}) + ); + + assert.ok(svg.includes(""), "Expected closing svg tag"); + }); + + test("generateOGImageSVG() generates matching snapshots for all test scenarios", () => { TEST_SCENARIOS.forEach((scenario) => { const statusDescription = StatusPage.getStatusDescription(scenario.statusCode); const statusColor = getStatusColor(scenario.statusCode); @@ -313,60 +331,89 @@ describe("generateOGImageSVG()", () => { }); }); -describe("StatusPage.generateOGImage()", () => { - test("converts SVG to valid PNG", async () => { - const { mockPage, cleanup } = createMockStatusPage( - { title: "Test Page" }, - { - heartbeats: [{ status: 1 }], - statusDescription: "All Systems Operational" - } - ); +describe("StatusPage Model", () => { + describe("overallStatus()", () => { + test("overallStatus() returns -1 when heartbeats array is empty", () => { + const status = StatusPage.overallStatus([]); + assert.strictEqual(status, -1); + }); - try { - const buffer = await StatusPage.generateOGImage(mockPage); - await assertValidPNG(buffer); - } finally { - cleanup(); - } + test("overallStatus() returns ALL_UP when all monitors are up", () => { + const status = StatusPage.overallStatus([{ status: 1 }, { status: 1 }]); + assert.strictEqual(status, STATUS_PAGE_ALL_UP); + }); + + test("overallStatus() returns PARTIAL_DOWN when monitors have mixed statuses", () => { + const status = StatusPage.overallStatus([{ status: 1 }, { status: 0 }]); + assert.strictEqual(status, STATUS_PAGE_PARTIAL_DOWN); + }); + + test("overallStatus() returns ALL_DOWN when all monitors are down", () => { + const status = StatusPage.overallStatus([{ status: 0 }, { status: 0 }]); + assert.strictEqual(status, STATUS_PAGE_ALL_DOWN); + }); }); - test("generates PNG with reasonable file size", async () => { - const { mockPage, cleanup } = createMockStatusPage( - {}, - { - heartbeats: [{ status: 1 }], - statusDescription: "All Systems Operational" - } - ); + describe("getStatusDescription()", () => { + test("getStatusDescription() returns 'No Services' when status is -1", () => { + assert.strictEqual(StatusPage.getStatusDescription(-1), "No Services"); + }); - try { - const buffer = await StatusPage.generateOGImage(mockPage); - assert.ok(buffer.length < 100 * 1024, "PNG should be less than 100KB"); - assert.ok(buffer.length > 1000, "PNG should be more than 1KB"); - } finally { - cleanup(); - } + test("getStatusDescription() returns 'All Systems Operational' when all systems are up", () => { + assert.strictEqual(StatusPage.getStatusDescription(STATUS_PAGE_ALL_UP), "All Systems Operational"); + }); + + test("getStatusDescription() returns 'Partially Degraded Service' when service is partially down", () => { + assert.strictEqual(StatusPage.getStatusDescription(STATUS_PAGE_PARTIAL_DOWN), "Partially Degraded Service"); + }); + + test("getStatusDescription() returns 'Degraded Service' when all systems are down", () => { + assert.strictEqual(StatusPage.getStatusDescription(STATUS_PAGE_ALL_DOWN), "Degraded Service"); + }); + + test("getStatusDescription() returns 'Under maintenance' when under maintenance", () => { + assert.strictEqual(StatusPage.getStatusDescription(MAINTENANCE), "Under maintenance"); + }); }); - - test("handles different status page configurations", async () => { - const { mockPage, cleanup } = createMockStatusPage( - { - title: "Production", - show_powered_by: false, - icon: "/icon.svg" - }, - { - heartbeats: [{ status: 1 }, { status: 0 }], - statusDescription: "Partially Degraded Service" + describe("StatusPage.generateOGImage()", () => { + test("generateOGImage() generates PNG between 1KB and 100KB", async () => { + const { mockPage, cleanup } = createMockStatusPage( + {}, + { + heartbeats: [{ status: 1 }], + statusDescription: "All Systems Operational" + } + ); + + try { + const buffer = await StatusPage.generateOGImage(mockPage); + await assertValidPNG(buffer); + assert.ok(buffer.length > 1000, "Expected PNG to be greater than 1KB"); + assert.ok(buffer.length < 100 * 1024, "Expected PNG to be less than 100KB"); + } finally { + cleanup(); } - ); - - try { - const buffer = await StatusPage.generateOGImage(mockPage); - await assertValidPNG(buffer); - } finally { - cleanup(); - } + }); + + test("generateOGImage() handles status page with custom icon and no branding", async () => { + const { mockPage, cleanup } = createMockStatusPage( + { + title: "Production", + show_powered_by: false, + icon: "/icon.svg" + }, + { + heartbeats: [{ status: 1 }, { status: 0 }], + statusDescription: "Partially Degraded Service" + } + ); + + try { + const buffer = await StatusPage.generateOGImage(mockPage); + await assertValidPNG(buffer); + } finally { + cleanup(); + } + }); }); }); From 6e79b77669d0be2e963c09a46764745f53d50a6b Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Fri, 9 Jan 2026 04:56:03 +0100 Subject: [PATCH 4/5] chore: add formatting and enable auto-formatting --- .eslintrc.js | 162 ++--- .github/workflows/autofix.yml | 45 ++ .prettierignore | 2 + .prettierrc.js | 65 ++ .stylelintrc | 7 +- package-lock.json | 1088 ++++++++------------------------- package.json | 20 +- 7 files changed, 440 insertions(+), 949 deletions(-) create mode 100644 .github/workflows/autofix.yml create mode 100644 .prettierignore create mode 100644 .prettierrc.js diff --git a/.eslintrc.js b/.eslintrc.js index ff3ec59c8..1e015f58e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,9 +1,5 @@ module.exports = { - ignorePatterns: [ - "test/*.js", - "server/modules/*", - "src/util.js" - ], + ignorePatterns: ["test/*.js", "server/modules/*", "src/util.js"], root: true, env: { browser: true, @@ -15,6 +11,7 @@ module.exports = { "eslint:recommended", "plugin:vue/vue3-recommended", "plugin:jsdoc/recommended-error", + "prettier", // Disables ESLint formatting rules that conflict with Prettier ], parser: "vue-eslint-parser", parserOptions: { @@ -22,148 +19,93 @@ module.exports = { sourceType: "module", requireConfigFile: false, }, - plugins: [ - "jsdoc", - "@typescript-eslint", - ], + plugins: ["jsdoc", "@typescript-eslint"], rules: { - "yoda": "error", - eqeqeq: [ "warn", "smart" ], - "linebreak-style": [ "error", "unix" ], - "camelcase": [ "warn", { - "properties": "never", - "ignoreImports": true - }], - "no-unused-vars": [ "warn", { - "args": "none" - }], - indent: [ - "error", - 4, + yoda: "error", + eqeqeq: ["warn", "smart"], + camelcase: [ + "warn", { - ignoredNodes: [ "TemplateLiteral" ], - SwitchCase: 1, + properties: "never", + ignoreImports: true, + }, + ], + "no-unused-vars": [ + "warn", + { + args: "none", }, ], - quotes: [ "error", "double" ], - semi: "error", - "vue/html-indent": [ "error", 4 ], // default: 2 "vue/max-attributes-per-line": "off", "vue/singleline-html-element-content-newline": "off", "vue/html-self-closing": "off", - "vue/require-component-is": "off", // not allow is="style" https://github.com/vuejs/eslint-plugin-vue/issues/462#issuecomment-430234675 - "vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly + "vue/require-component-is": "off", // not allow is="style" https://github.com/vuejs/eslint-plugin-vue/issues/462#issuecomment-430234675 + "vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly "vue/multi-word-component-names": "off", - "no-multi-spaces": [ "error", { - ignoreEOLComments: true, - }], - "array-bracket-spacing": [ "warn", "always", { - "singleValue": true, - "objectsInArrays": false, - "arraysInArrays": false - }], - "space-before-function-paren": [ "error", { - "anonymous": "always", - "named": "never", - "asyncArrow": "always" - }], - "curly": "error", - "object-curly-spacing": [ "error", "always" ], - "object-curly-newline": "off", - "object-property-newline": "error", - "comma-spacing": "error", - "brace-style": "error", + curly: "error", "no-var": "error", - "key-spacing": "warn", - "keyword-spacing": "warn", - "space-infix-ops": "error", - "arrow-spacing": "warn", "no-throw-literal": "error", - "no-trailing-spaces": "error", - "no-constant-condition": [ "error", { - "checkLoops": false, - }], - "space-before-blocks": "warn", + "no-constant-condition": [ + "error", + { + checkLoops: false, + }, + ], //"no-console": "warn", "no-extra-boolean-cast": "off", - "no-multiple-empty-lines": [ "warn", { - "max": 1, - "maxBOF": 0, - }], - "lines-between-class-members": [ "warn", "always", { - exceptAfterSingleLine: true, - }], "no-unneeded-ternary": "error", - "array-bracket-newline": [ "error", "consistent" ], - "eol-last": [ "error", "always" ], //"prefer-template": "error", - "template-curly-spacing": [ "warn", "never" ], - "comma-dangle": [ "warn", "only-multiline" ], - "no-empty": [ "error", { - "allowEmptyCatch": true - }], + "no-empty": [ + "error", + { + allowEmptyCatch: true, + }, + ], "no-control-regex": "off", - "one-var": [ "error", "never" ], - "max-statements-per-line": [ "error", { "max": 1 }], + "one-var": ["error", "never"], + "max-statements-per-line": ["error", { max: 1 }], "jsdoc/check-tag-names": [ "error", { - "definedTags": [ "link" ] - } + definedTags: ["link"], + }, ], "jsdoc/no-undefined-types": "off", - "jsdoc/no-defaults": [ - "error", - { "noOptionalParamNames": true } - ], + "jsdoc/no-defaults": ["error", { noOptionalParamNames: true }], "jsdoc/require-throws": "warn", "jsdoc/require-jsdoc": [ "error", { - "require": { - "FunctionDeclaration": true, - "MethodDefinition": true, - } - } + require: { + FunctionDeclaration: true, + MethodDefinition: true, + }, + }, ], "jsdoc/no-blank-block-descriptions": "error", "jsdoc/require-returns-description": "warn", - "jsdoc/require-returns-check": [ - "error", - { "reportMissingReturnForUndefinedTypes": false } - ], + "jsdoc/require-returns-check": ["error", { reportMissingReturnForUndefinedTypes: false }], "jsdoc/require-returns": [ "warn", { - "forceRequireReturn": true, - "forceReturnsWithAsync": true - } + forceRequireReturn: true, + forceReturnsWithAsync: true, + }, ], "jsdoc/require-param-type": "warn", - "jsdoc/require-param-description": "warn" + "jsdoc/require-param-description": "warn", }, - "overrides": [ - { - "files": [ "src/languages/*.js", "src/icon.js" ], - "rules": { - "comma-dangle": [ "error", "always-multiline" ], - } - }, - + overrides: [ // Override for TypeScript { - "files": [ - "**/*.ts", - ], - extends: [ - "plugin:@typescript-eslint/recommended", - ], - "rules": { + files: ["**/*.ts"], + extends: ["plugin:@typescript-eslint/recommended"], + rules: { "jsdoc/require-returns-type": "off", "jsdoc/require-param-type": "off", "@typescript-eslint/no-explicit-any": "off", "prefer-const": "off", - } - } - ] + }, + }, + ], }; diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 000000000..617f8bee1 --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,45 @@ +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 + + - name: Auto-format code with Prettier + run: npm run fmt + continue-on-error: true + + - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..9fdef271a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +# language files +src/lang/*.json diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000..bee4a2013 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,65 @@ +/** + * Prettier Configuration for Uptime Kuma + * + * Usage: + * npm run fmt - Format all files (auto-runs in CI via autofix workflow) + * npm run fmt -- --check - Check formatting without making changes + * + * TIP: This formatter is automatically run in CI, so no need to worry about it + */ +module.exports = { + // Core formatting options - matching original ESLint rules + semi: true, + singleQuote: false, + trailingComma: "es5", + printWidth: 120, + tabWidth: 4, + useTabs: false, + endOfLine: "lf", + arrowParens: "always", + bracketSpacing: true, + bracketSameLine: false, + + // Vue-specific settings + vueIndentScriptAndStyle: false, + singleAttributePerLine: false, + htmlWhitespaceSensitivity: "ignore", // More forgiving with whitespace in HTML + + // Override settings for specific file types + overrides: [ + { + files: "*.vue", + options: { + parser: "vue", + }, + }, + { + files: ["*.json"], + options: { + tabWidth: 4, + trailingComma: "none", + }, + }, + { + files: ["*.yml", "*.yaml"], + options: { + tabWidth: 2, + trailingComma: "none", + }, + }, + { + files: ["src/icon.js"], + options: { + trailingComma: "all", + }, + }, + { + files: ["*.md"], + options: { + printWidth: 100, + proseWrap: "preserve", + tabWidth: 2, + }, + }, + ], +}; diff --git a/.stylelintrc b/.stylelintrc index 0bcdb7c27..c0b540d69 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -1,10 +1,11 @@ { - "extends": "stylelint-config-standard", + "extends": [ + "stylelint-config-standard", + "stylelint-config-prettier" + ], "customSyntax": "postcss-html", "rules": { - "indentation": 4, "no-descending-specificity": null, - "selector-list-comma-newline-after": null, "declaration-empty-line-before": null, "alpha-value-notation": "number", "color-function-notation": "legacy", diff --git a/package-lock.json b/package-lock.json index 415332d13..0de4bdf23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "uptime-kuma", - "version": "2.1.0-beta.0", + "version": "2.1.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "uptime-kuma", - "version": "2.1.0-beta.0", + "version": "2.1.0-beta.1", "license": "MIT", "dependencies": { "@grpc/grpc-js": "~1.8.22", @@ -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", @@ -62,7 +62,7 @@ "node-cloudflared-tunnel": "~1.0.9", "node-fetch-cache": "^5.1.0", "node-radius-utils": "~1.2.0", - "nodemailer": "~6.9.13", + "nodemailer": "~7.0.12", "nostr-tools": "^2.10.4", "notp": "~2.0.3", "openid-client": "^5.4.2", @@ -74,12 +74,11 @@ "prometheus-api-metrics": "~3.2.1", "promisify-child-process": "~4.1.2", "protobufjs": "~7.2.4", - "qs": "~6.10.4", + "qs": "~6.14.1", "radius": "~1.1.4", "redbean-node": "~0.3.0", "redis": "~5.9.0", "semver": "~7.5.4", - "sharp": "~0.34.5", "socket.io": "~4.8.0", "socket.io-client": "~4.8.0", "socks-proxy-agent": "~8.0.5", @@ -89,6 +88,7 @@ "thirty-two": "~1.0.2", "tldts": "^7.0.19", "tough-cookie": "~4.1.3", + "validator": "^13.15.26", "web-push": "^3.6.7", "ws": "^8.13.0" }, @@ -101,7 +101,9 @@ "@playwright/test": "~1.39.0", "@popperjs/core": "~2.10.2", "@testcontainers/hivemq": "^10.13.1", + "@testcontainers/mariadb": "^10.13.0", "@testcontainers/mssqlserver": "^10.28.0", + "@testcontainers/mysql": "^11.11.0", "@testcontainers/postgresql": "^11.9.0", "@testcontainers/rabbitmq": "^10.13.2", "@types/bootstrap": "~5.1.9", @@ -124,6 +126,7 @@ "dns2": "~2.0.1", "dompurify": "~3.2.4", "eslint": "~8.14.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-jsdoc": "~46.4.6", "eslint-plugin-vue": "~8.7.1", "favico.js": "~0.3.10", @@ -132,11 +135,13 @@ "postcss-html": "~1.5.0", "postcss-rtlcss": "~3.7.2", "postcss-scss": "~4.0.4", + "prettier": "^3.7.4", "prismjs": "~1.30.0", "qrcode": "~1.5.0", "rollup-plugin-visualizer": "^5.6.0", "sass": "~1.42.1", "stylelint": "^15.10.1", + "stylelint-config-prettier": "^9.0.5", "stylelint-config-standard": "~25.0.0", "terser": "~5.15.0", "test": "~3.3.0", @@ -2939,16 +2944,6 @@ "postcss-selector-parser": "^6.0.13" } }, - "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@es-joy/jsdoccomment": { "version": "0.40.1", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", @@ -3609,471 +3604,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@intlify/core-base": { "version": "9.14.5", "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz", @@ -5432,12 +4962,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" @@ -6120,6 +5650,16 @@ "testcontainers": "^10.28.0" } }, + "node_modules/@testcontainers/mariadb": { + "version": "10.28.0", + "resolved": "https://registry.npmjs.org/@testcontainers/mariadb/-/mariadb-10.28.0.tgz", + "integrity": "sha512-+ETpRbHOWxEj6uwMfhTVvE6ap0U+olD+v8XbAE2+88YgsHzlmfWWi/EXsOfW1VZsWblYE5kR0k1O//a9Sei4Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "testcontainers": "^10.28.0" + } + }, "node_modules/@testcontainers/mssqlserver": { "version": "10.28.0", "resolved": "https://registry.npmjs.org/@testcontainers/mssqlserver/-/mssqlserver-10.28.0.tgz", @@ -6130,6 +5670,63 @@ "testcontainers": "^10.28.0" } }, + "node_modules/@testcontainers/mysql": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@testcontainers/mysql/-/mysql-11.11.0.tgz", + "integrity": "sha512-2EfFhUDEvEdwBwez+F/NhqP+h2rFzLzHYbRX0N/9/Lgdlq8TbsYWZ9SaWL9V0f1FWX89XnyZrT3i/j7m8MIESg==", + "dev": true, + "license": "MIT", + "dependencies": { + "testcontainers": "^11.11.0" + } + }, + "node_modules/@testcontainers/mysql/node_modules/docker-compose": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.3.0.tgz", + "integrity": "sha512-7Gevk/5eGD50+eMD+XDnFnOrruFkL0kSd7jEG4cjmqweDSUhB7i0g8is/nBdVpl+Bx338SqIB2GLKm32M+Vs6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@testcontainers/mysql/node_modules/testcontainers": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-11.11.0.tgz", + "integrity": "sha512-nKTJn3n/gkyGg/3SVkOwX+isPOGSHlfI+CWMobSmvQrsj7YW01aWvl2pYIfV4LMd+C8or783yYrzKSK2JlP+Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@types/dockerode": "^3.3.47", + "archiver": "^7.0.1", + "async-lock": "^1.4.1", + "byline": "^5.0.0", + "debug": "^4.4.3", + "docker-compose": "^1.3.0", + "dockerode": "^4.0.9", + "get-port": "^7.1.0", + "proper-lockfile": "^4.1.2", + "properties-reader": "^2.3.0", + "ssh-remote-port-forward": "^1.0.4", + "tar-fs": "^3.1.1", + "tmp": "^0.2.5", + "undici": "^7.16.0" + } + }, + "node_modules/@testcontainers/mysql/node_modules/undici": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", + "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/@testcontainers/postgresql": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-11.11.0.tgz", @@ -6236,18 +5833,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", @@ -6369,15 +5954,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", @@ -6460,15 +6036,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", @@ -8296,54 +7863,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": { @@ -8676,18 +8219,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", @@ -10019,6 +9550,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", @@ -10409,6 +9948,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-plugin-jsdoc": { "version": "46.4.6", "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.4.6.tgz", @@ -10841,7 +10396,6 @@ } ], "license": "MIT", - "optional": true, "dependencies": { "strnum": "^2.1.0" }, @@ -11162,10 +10716,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", @@ -11370,171 +10927,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": { @@ -11891,27 +11319,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" @@ -14099,12 +13525,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": { @@ -14129,10 +13558,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", @@ -14583,6 +14015,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", @@ -14814,9 +14252,9 @@ } }, "node_modules/nodemailer": { - "version": "6.9.16", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", - "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz", + "integrity": "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -14864,12 +14302,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" @@ -15739,6 +15177,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", @@ -16133,12 +15587,12 @@ } }, "node_modules/qs": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", - "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -16748,26 +16202,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", @@ -17268,62 +16716,6 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/sharp/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -17786,6 +17178,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", @@ -18084,8 +17485,7 @@ "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/style-search": { "version": "0.1.0", @@ -18153,6 +17553,23 @@ "url": "https://opencollective.com/stylelint" } }, + "node_modules/stylelint-config-prettier": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/stylelint-config-prettier/-/stylelint-config-prettier-9.0.5.tgz", + "integrity": "sha512-U44lELgLZhbAD/xy/vncZ2Pq8sh2TnpiPvo38Ifg9+zeioR+LAkHu0i6YORIOxFafZoVg0xqQwex6e6F25S5XA==", + "dev": true, + "license": "MIT", + "bin": { + "stylelint-config-prettier": "bin/check.js", + "stylelint-config-prettier-check": "bin/check.js" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "stylelint": ">= 11.x < 15" + } + }, "node_modules/stylelint-config-recommended": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz", @@ -18501,6 +17918,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", @@ -19274,6 +18705,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/varint": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", @@ -19662,16 +19102,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 791c1cf25..496c185c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uptime-kuma", - "version": "2.1.0-beta.0", + "version": "2.1.0-beta.1", "license": "MIT", "repository": { "type": "git", @@ -16,6 +16,7 @@ "lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore", "lint-fix:style": "stylelint \"**/*.{vue,css,scss}\" --fix --ignore-path .gitignore", "lint": "npm run lint:js && npm run lint:style", + "fmt": "prettier --write \"**/*.{js,ts,vue,css,scss,json,md,yml,yaml}\"", "lint:prod": "npm run lint:js-prod && npm run lint:style", "dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"", "start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js", @@ -28,8 +29,8 @@ "test": "npm run test-backend && npm run test-e2e", "test-with-build": "npm run build && npm test", "test-backend": "node test/test-backend.mjs", - "test-backend-22": "cross-env TEST_BACKEND=1 node --test \"test/backend-test/**/*.js\"", - "test-backend-20": "cross-env TEST_BACKEND=1 node --test test/backend-test", + "test-backend-22": "cross-env TEST_BACKEND=1 node --test --test-reporter=spec \"test/backend-test/**/*.js\"", + "test-backend-20": "cross-env TEST_BACKEND=1 node --test --test-reporter=spec test/backend-test", "test-e2e": "playwright test --config ./config/playwright.config.js", "test-e2e-ui": "playwright test --config ./config/playwright.config.js --ui --ui-port=51063", "playwright-codegen": "playwright codegen localhost:3000 --save-storage=./private/e2e-auth.json", @@ -95,7 +96,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", @@ -123,7 +124,7 @@ "node-cloudflared-tunnel": "~1.0.9", "node-fetch-cache": "^5.1.0", "node-radius-utils": "~1.2.0", - "nodemailer": "~6.9.13", + "nodemailer": "~7.0.12", "nostr-tools": "^2.10.4", "notp": "~2.0.3", "openid-client": "^5.4.2", @@ -135,12 +136,11 @@ "prometheus-api-metrics": "~3.2.1", "promisify-child-process": "~4.1.2", "protobufjs": "~7.2.4", - "qs": "~6.10.4", + "qs": "~6.14.1", "radius": "~1.1.4", "redbean-node": "~0.3.0", "redis": "~5.9.0", "semver": "~7.5.4", - "sharp": "~0.34.5", "socket.io": "~4.8.0", "socket.io-client": "~4.8.0", "socks-proxy-agent": "~8.0.5", @@ -150,6 +150,7 @@ "thirty-two": "~1.0.2", "tldts": "^7.0.19", "tough-cookie": "~4.1.3", + "validator": "^13.15.26", "web-push": "^3.6.7", "ws": "^8.13.0" }, @@ -162,7 +163,9 @@ "@playwright/test": "~1.39.0", "@popperjs/core": "~2.10.2", "@testcontainers/hivemq": "^10.13.1", + "@testcontainers/mariadb": "^10.13.0", "@testcontainers/mssqlserver": "^10.28.0", + "@testcontainers/mysql": "^11.11.0", "@testcontainers/postgresql": "^11.9.0", "@testcontainers/rabbitmq": "^10.13.2", "@types/bootstrap": "~5.1.9", @@ -185,6 +188,7 @@ "dns2": "~2.0.1", "dompurify": "~3.2.4", "eslint": "~8.14.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-jsdoc": "~46.4.6", "eslint-plugin-vue": "~8.7.1", "favico.js": "~0.3.10", @@ -193,11 +197,13 @@ "postcss-html": "~1.5.0", "postcss-rtlcss": "~3.7.2", "postcss-scss": "~4.0.4", + "prettier": "^3.7.4", "prismjs": "~1.30.0", "qrcode": "~1.5.0", "rollup-plugin-visualizer": "^5.6.0", "sass": "~1.42.1", "stylelint": "^15.10.1", + "stylelint-config-prettier": "^9.0.5", "stylelint-config-standard": "~25.0.0", "terser": "~5.15.0", "test": "~3.3.0", From 35feb7de79b4761dd7401ad6f220f66e6c85cc50 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 04:05:18 +0000 Subject: [PATCH 5/5] [autofix.ci] apply automated fixes --- server/model/status_page.js | 9 +- server/routers/status-page-router.js | 5 +- server/utils/og-image.js | 32 +---- .../backend-test/test-status-page-og-image.js | 124 +++++------------- test/e2e/specs/status-page.spec.js | 2 +- 5 files changed, 47 insertions(+), 125 deletions(-) diff --git a/server/model/status_page.js b/server/model/status_page.js index 4984ce429..fe986fee3 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -317,7 +317,7 @@ class StatusPage extends BeanModel { for (const monitor of group.monitorList) { monitors.push({ name: monitor.name, - status: monitor.status || 2 // 1=up, 0=down, 2=pending + status: monitor.status || 2, // 1=up, 0=down, 2=pending }); } } @@ -330,9 +330,8 @@ class StatusPage extends BeanModel { } // Get icon - use getIcon() method if available, otherwise use icon property - const icon = (statusPage.getIcon && typeof statusPage.getIcon === "function") - ? statusPage.getIcon() - : statusPage.icon; + const icon = + statusPage.getIcon && typeof statusPage.getIcon === "function" ? statusPage.getIcon() : statusPage.icon; // If no detailed monitor data, create array with count for display const monitorsForDisplay = monitors.length > 0 ? monitors : heartbeats.map(() => ({})); @@ -344,7 +343,7 @@ class StatusPage extends BeanModel { statusColor, icon, showPoweredBy: !!statusPage.show_powered_by, - monitors: monitorsForDisplay + monitors: monitorsForDisplay, }); } diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index 3b960c337..792e5abd0 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -245,9 +245,7 @@ router.get("/api/status-page/:slug/image", cache("5 minutes"), async (request, r try { // Get Status Page - let statusPage = await R.findOne("status_page", " slug = ? ", [ - slug - ]); + let statusPage = await R.findOne("status_page", " slug = ? ", [slug]); if (!statusPage) { sendHttpError(response, "Status Page Not Found"); @@ -261,7 +259,6 @@ router.get("/api/status-page/:slug/image", cache("5 minutes"), async (request, r response.type("image/png"); response.setHeader("Cache-Control", "public, max-age=300"); // 5 minutes response.send(pngBuffer); - } catch (error) { sendHttpError(response, error.message); } diff --git a/server/utils/og-image.js b/server/utils/og-image.js index 35199af84..35654fb84 100644 --- a/server/utils/og-image.js +++ b/server/utils/og-image.js @@ -9,7 +9,7 @@ const { MAINTENANCE, UP, DOWN, - PENDING + PENDING, } = require("../util-server"); // Image dimensions (Open Graph standard) @@ -94,10 +94,7 @@ function truncateText(text, maxLength) { * @returns {object} Status counts */ function countMonitorsByStatus(monitors) { - const counts = { up: 0, - down: 0, - pending: 0, - maintenance: 0 }; + const counts = { up: 0, down: 0, pending: 0, maintenance: 0 }; monitors.forEach((monitor) => { if (monitor.status === UP) { @@ -165,7 +162,7 @@ function generateIndividualMonitorDetails(monitors, startY) { const displayMonitors = monitors.slice(0, MAX_INDIVIDUAL_MONITORS); displayMonitors.forEach((monitor, index) => { - const y = startY + (index * 35); + const y = startY + index * 35; const statusColor = getMonitorStatusColor(monitor.status); const monitorName = truncateText(monitor.name, MAX_MONITOR_NAME_LENGTH); @@ -306,28 +303,11 @@ ${monitorDetailsSVG}${footerSVG} * @returns {Promise} PNG image buffer */ async function generateOGImage(statusPageData) { - const { - title, - statusDescription, - statusColor, - icon, - showPoweredBy, - monitors - } = statusPageData; + const { title, statusDescription, statusColor, icon, showPoweredBy, monitors } = statusPageData; - const svg = generateOGImageSVG( - title, - statusDescription, - statusColor, - icon, - !!showPoweredBy, - Date.now(), - monitors - ); + const svg = generateOGImageSVG(title, statusDescription, statusColor, icon, !!showPoweredBy, Date.now(), monitors); - const pngBuffer = await sharp(Buffer.from(svg)) - .png() - .toBuffer(); + const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer(); return pngBuffer; } diff --git a/test/backend-test/test-status-page-og-image.js b/test/backend-test/test-status-page-og-image.js index 68bafee5e..39968707e 100644 --- a/test/backend-test/test-status-page-og-image.js +++ b/test/backend-test/test-status-page-og-image.js @@ -5,12 +5,7 @@ const StatusPage = require("../../server/model/status_page"); const { generateOGImageSVG, getStatusColor, escapeXml } = require("../../server/utils/og-image"); const fs = require("fs"); const path = require("path"); -const { - STATUS_PAGE_ALL_UP, - STATUS_PAGE_PARTIAL_DOWN, - STATUS_PAGE_ALL_DOWN, - MAINTENANCE -} = require("../../src/util"); +const { STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, STATUS_PAGE_ALL_DOWN, MAINTENANCE } = require("../../src/util"); const SNAPSHOTS_DIR = path.join(__dirname, "snapshots", "og-images"); @@ -27,38 +22,38 @@ const TEST_SCENARIOS = [ name: "all-up", title: "Production", statusCode: STATUS_PAGE_ALL_UP, - monitorCount: 8 + monitorCount: 8, }, { name: "all-down", title: "Infrastructure", statusCode: STATUS_PAGE_ALL_DOWN, - monitorCount: 6 + monitorCount: 6, }, { name: "maintenance", title: "Maintenance", statusCode: MAINTENANCE, - monitorCount: 3 + monitorCount: 3, }, { name: "partial", title: "API Services", statusCode: STATUS_PAGE_PARTIAL_DOWN, - monitorCount: 12 + monitorCount: 12, }, { name: "all-up-many-monitors", title: "Enterprise", statusCode: STATUS_PAGE_ALL_UP, - monitorCount: 200 + monitorCount: 200, }, { name: "all-up-no-branding", title: "Production", statusCode: STATUS_PAGE_ALL_UP, monitorCount: 8, - showPoweredBy: false + showPoweredBy: false, }, { name: "all-up-custom-icon-no-branding", @@ -66,7 +61,7 @@ const TEST_SCENARIOS = [ statusCode: STATUS_PAGE_ALL_UP, monitorCount: 8, icon: "/icon.svg", - showPoweredBy: false + showPoweredBy: false, }, { name: "all-up-custom-icon-with-branding", @@ -74,14 +69,14 @@ const TEST_SCENARIOS = [ statusCode: STATUS_PAGE_ALL_UP, monitorCount: 8, icon: "/icon.svg", - showPoweredBy: true + showPoweredBy: true, }, { name: "all-up-long-title", title: "This is an extremely long status page title that will need to be truncated", statusCode: STATUS_PAGE_ALL_UP, - monitorCount: 5 - } + monitorCount: 5, + }, ]; /** @@ -114,7 +109,7 @@ function createMockStatusPage(overrides = {}, rssData = null) { slug: "test", title: "Test Page", description: "Test Description", - ...overrides + ...overrides, }; const originalGetRSSData = StatusPage.getRSSPageData; @@ -127,8 +122,7 @@ function createMockStatusPage(overrides = {}, rssData = null) { StatusPage.getRSSPageData = originalGetRSSData; }; - return { mockPage, - cleanup }; + return { mockPage, cleanup }; } /** @@ -141,13 +135,9 @@ async function assertValidPNG(buffer) { assert.ok(buffer.length > 0, "Expected non-empty buffer"); // Check PNG signature (first 8 bytes) - const pngSignature = Buffer.from([ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ]); + const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); const actualSignature = buffer.slice(0, 8); - assert.deepStrictEqual( - actualSignature, - pngSignature, - "Expected valid PNG signature" - ); + assert.deepStrictEqual(actualSignature, pngSignature, "Expected valid PNG signature"); // Use sharp to verify metadata const metadata = await sharp(buffer).metadata(); @@ -171,7 +161,7 @@ describe("OG Image Helper Functions", () => { }); test("escapeXml() replaces double quote with entity", () => { - assert.strictEqual(escapeXml("\""), """); + assert.strictEqual(escapeXml('"'), """); }); test("escapeXml() replaces single quote with entity", () => { @@ -210,71 +200,31 @@ describe("OG Image Helper Functions", () => { describe("generateOGImageSVG()", () => { test("generateOGImageSVG() starts with XML declaration", () => { - const svg = generateOGImageSVG( - "Test", - "All OK", - "#10b981", - null, - true, - FIXED_TIMESTAMP, - Array(5).fill({}) - ); + const svg = generateOGImageSVG("Test", "All OK", "#10b981", null, true, FIXED_TIMESTAMP, Array(5).fill({})); assert.ok(svg.startsWith(" { - const svg = generateOGImageSVG( - "Test", - "All OK", - "#10b981", - null, - true, - FIXED_TIMESTAMP, - Array(5).fill({}) - ); + const svg = generateOGImageSVG("Test", "All OK", "#10b981", null, true, FIXED_TIMESTAMP, Array(5).fill({})); - assert.ok(svg.includes("xmlns=\"http://www.w3.org/2000/svg\""), "Expected SVG namespace"); + assert.ok(svg.includes('xmlns="http://www.w3.org/2000/svg"'), "Expected SVG namespace"); }); test("generateOGImageSVG() sets width to 1200", () => { - const svg = generateOGImageSVG( - "Test", - "All OK", - "#10b981", - null, - true, - FIXED_TIMESTAMP, - Array(5).fill({}) - ); + const svg = generateOGImageSVG("Test", "All OK", "#10b981", null, true, FIXED_TIMESTAMP, Array(5).fill({})); - assert.ok(svg.includes("width=\"1200\""), "Expected width of 1200"); + assert.ok(svg.includes('width="1200"'), "Expected width of 1200"); }); test("generateOGImageSVG() sets height to 630", () => { - const svg = generateOGImageSVG( - "Test", - "All OK", - "#10b981", - null, - true, - FIXED_TIMESTAMP, - Array(5).fill({}) - ); + const svg = generateOGImageSVG("Test", "All OK", "#10b981", null, true, FIXED_TIMESTAMP, Array(5).fill({})); - assert.ok(svg.includes("height=\"630\""), "Expected height of 630"); + assert.ok(svg.includes('height="630"'), "Expected height of 630"); }); test("generateOGImageSVG() includes closing SVG tag", () => { - const svg = generateOGImageSVG( - "Test", - "All OK", - "#10b981", - null, - true, - FIXED_TIMESTAMP, - Array(5).fill({}) - ); + const svg = generateOGImageSVG("Test", "All OK", "#10b981", null, true, FIXED_TIMESTAMP, Array(5).fill({})); assert.ok(svg.includes(""), "Expected closing svg tag"); }); @@ -293,25 +243,21 @@ describe("generateOGImageSVG()", () => { // Create realistic monitor mix for status summary monitors = []; const upCount = Math.floor(scenario.monitorCount * 0.85); // 85% up - const downCount = Math.floor(scenario.monitorCount * 0.10); // 10% down + const downCount = Math.floor(scenario.monitorCount * 0.1); // 10% down const maintenanceCount = Math.floor(scenario.monitorCount * 0.03); // 3% maintenance const pendingCount = scenario.monitorCount - upCount - downCount - maintenanceCount; // Rest pending for (let i = 0; i < upCount; i++) { - monitors.push({ name: `Monitor ${i + 1}`, - status: 1 }); + monitors.push({ name: `Monitor ${i + 1}`, status: 1 }); } for (let i = 0; i < downCount; i++) { - monitors.push({ name: `Monitor ${upCount + i + 1}`, - status: 0 }); + monitors.push({ name: `Monitor ${upCount + i + 1}`, status: 0 }); } for (let i = 0; i < maintenanceCount; i++) { - monitors.push({ name: `Monitor ${upCount + downCount + i + 1}`, - status: 3 }); + monitors.push({ name: `Monitor ${upCount + downCount + i + 1}`, status: 3 }); } for (let i = 0; i < pendingCount; i++) { - monitors.push({ name: `Monitor ${upCount + downCount + maintenanceCount + i + 1}`, - status: 2 }); + monitors.push({ name: `Monitor ${upCount + downCount + maintenanceCount + i + 1}`, status: 2 }); } } else { monitors = Array(scenario.monitorCount).fill({}); // Show count @@ -381,10 +327,10 @@ describe("StatusPage Model", () => { {}, { heartbeats: [{ status: 1 }], - statusDescription: "All Systems Operational" + statusDescription: "All Systems Operational", } ); - + try { const buffer = await StatusPage.generateOGImage(mockPage); await assertValidPNG(buffer); @@ -394,20 +340,20 @@ describe("StatusPage Model", () => { cleanup(); } }); - + test("generateOGImage() handles status page with custom icon and no branding", async () => { const { mockPage, cleanup } = createMockStatusPage( { title: "Production", show_powered_by: false, - icon: "/icon.svg" + icon: "/icon.svg", }, { heartbeats: [{ status: 1 }, { status: 0 }], - statusDescription: "Partially Degraded Service" + statusDescription: "Partially Degraded Service", } ); - + try { const buffer = await StatusPage.generateOGImage(mockPage); await assertValidPNG(buffer); diff --git a/test/e2e/specs/status-page.spec.js b/test/e2e/specs/status-page.spec.js index 29d95f7c4..600f854bf 100644 --- a/test/e2e/specs/status-page.spec.js +++ b/test/e2e/specs/status-page.spec.js @@ -239,7 +239,7 @@ test.describe("Status Page", () => { expect(buffer.length).toBeGreaterThan(0); // Verify PNG signature - const pngSignature = Buffer.from([ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ]); + const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); expect(buffer.subarray(0, 8)).toEqual(pngSignature); // Test 404 for non-existent page