From 18fa8cef2b80bd48da9acf2485e279d7451a4155 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sat, 6 Jun 2026 12:02:38 +0800 Subject: [PATCH 01/35] Improvements --- .env.example | 1 - package.json | 18 +- pnpm-lock.yaml | 590 +++++++++++++++++++++++++------------------------ 3 files changed, 316 insertions(+), 293 deletions(-) diff --git a/.env.example b/.env.example index bce012123..3267ed4d4 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,6 @@ BACKEND_PORT="11966" FRONTEND_PORT="18966" ALLOWED_DOMAINS="" # APIs -BING_MAP_API_KEY="" GOOGLE_MAP_API_KEY="" IPINFO_API_TOKEN="" CLOUDFLARE_API="" diff --git a/package.json b/package.json index 9a7e44bb2..2ae1d4a4b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "version": "6.4.0", "type": "module", - "packageManager": "pnpm@11.5.0+sha512.dbfcc4f81cf48597afd4bc391ffdf12c11f1a9fb83a395bfa6b0a2d9cc2fd8ffebafdb1ccbd529632153f793904c2615b7f09fe1a345473fd1c35845172a8eb1", + "packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916", "scripts": { "dev": "concurrently \"vite\" \"nodemon backend-server.js\"", "build": "vite build", @@ -15,11 +15,11 @@ "start": "concurrently \"node frontend-server.js\" \"node backend-server.js\"" }, "dependencies": { - "@cloudflare/speedtest": "^1.9.1", + "@cloudflare/speedtest": "^1.10.1", "@iconify-json/circle-flags": "^1.2.10", "@iconify/vue": "^5.0.1", "@khmyznikov/pwa-install": "^0.6.3", - "@lucide/vue": "^1.16.0", + "@lucide/vue": "^1.17.0", "@tanstack/vue-table": "^8.21.3", "@thumbmarkjs/thumbmarkjs": "^1.9.1", "@vueuse/core": "^14.3.0", @@ -34,7 +34,7 @@ "express": "^5.2.1", "express-rate-limit": "^8.5.2", "express-slow-down": "^3.1.0", - "firebase": "^12.13.0", + "firebase": "^12.14.0", "html-to-image": "^1.11.13", "http-proxy-middleware": "^4.0.0", "maxmind": "^5.0.6", @@ -42,10 +42,10 @@ "pino": "^10.3.1", "pino-http": "^11.0.0", "pino-pretty": "^13.1.3", - "reka-ui": "^2.9.8", - "svgmap": "2.20.0", + "reka-ui": "^2.9.9", + "svgmap": "2.20.1", "tailwind-merge": "^3.6.0", - "tar": "^7.5.15", + "tar": "^7.5.16", "tw-animate-css": "^1.4.0", "ua-parser-js": "^2.0.10", "unbzip2-stream": "^1.4.3", @@ -53,7 +53,7 @@ "vue": "^3.5.35", "vue-i18n": "^11.4.4", "vue-markdown-render": "^2.3.0", - "vue-router": "^5.0.7", + "vue-router": "^5.1.0", "vue-sonner": "^2.0.9", "whoiser": "^1.18.0" }, @@ -64,6 +64,6 @@ "nodemon": "^3.1.14", "tailwindcss": "^4.3.0", "vconsole": "^3.15.1", - "vite": "^8.0.14" + "vite": "^8.0.16" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee0f3e454..60c425285 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@cloudflare/speedtest': - specifier: ^1.9.1 - version: 1.9.1 + specifier: ^1.10.1 + version: 1.10.1 '@iconify-json/circle-flags': specifier: ^1.2.10 version: 1.2.10 @@ -21,8 +21,8 @@ importers: specifier: ^0.6.3 version: 0.6.3(@lit/react@1.0.8(@types/react@19.2.14))(@types/dom-chromium-installation-events@101.0.4)(@types/web-app-manifest@1.0.9)(lit@3.3.2) '@lucide/vue': - specifier: ^1.16.0 - version: 1.16.0(vue@3.5.35) + specifier: ^1.17.0 + version: 1.17.0(vue@3.5.35) '@tanstack/vue-table': specifier: ^8.21.3 version: 8.21.3(vue@3.5.35) @@ -58,22 +58,22 @@ importers: version: 17.4.2 express: specifier: ^5.2.1 - version: 5.2.1 + version: 5.2.1(supports-color@5.5.0) express-rate-limit: specifier: ^8.5.2 - version: 8.5.2(express@5.2.1) + version: 8.5.2(express@5.2.1(supports-color@5.5.0)) express-slow-down: specifier: ^3.1.0 - version: 3.1.0(express@5.2.1) + version: 3.1.0(express@5.2.1(supports-color@5.5.0)) firebase: - specifier: ^12.13.0 - version: 12.13.0 + specifier: ^12.14.0 + version: 12.14.0 html-to-image: specifier: ^1.11.13 version: 1.11.13 http-proxy-middleware: specifier: ^4.0.0 - version: 4.0.0 + version: 4.0.0(supports-color@5.5.0) maxmind: specifier: ^5.0.6 version: 5.0.6 @@ -90,17 +90,17 @@ importers: specifier: ^13.1.3 version: 13.1.3 reka-ui: - specifier: ^2.9.8 - version: 2.9.8(vue@3.5.35) + specifier: ^2.9.9 + version: 2.9.9(vue@3.5.35) svgmap: - specifier: 2.20.0 - version: 2.20.0 + specifier: 2.20.1 + version: 2.20.1 tailwind-merge: specifier: ^3.6.0 version: 3.6.0 tar: - specifier: ^7.5.15 - version: 7.5.15 + specifier: ^7.5.16 + version: 7.5.16 tw-animate-css: specifier: ^1.4.0 version: 1.4.0 @@ -112,7 +112,7 @@ importers: version: 1.4.3 vaul-vue: specifier: ^0.4.1 - version: 0.4.1(reka-ui@2.9.8(vue@3.5.35))(vue@3.5.35) + version: 0.4.1(reka-ui@2.9.9(vue@3.5.35))(vue@3.5.35) vue: specifier: ^3.5.35 version: 3.5.35 @@ -123,8 +123,8 @@ importers: specifier: ^2.3.0 version: 2.3.0(vue@3.5.35) vue-router: - specifier: ^5.0.7 - version: 5.0.7(@vue/compiler-sfc@3.5.35)(pinia@3.0.4(vue@3.5.35))(vue@3.5.35) + specifier: ^5.1.0 + version: 5.1.0(@vue/compiler-sfc@3.5.35)(pinia@3.0.4(vue@3.5.35))(vite@8.0.16(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.9.0))(vue@3.5.35) vue-sonner: specifier: ^2.0.9 version: 2.0.9 @@ -134,13 +134,13 @@ importers: devDependencies: '@tailwindcss/vite': specifier: ^4.3.0 - version: 4.3.0(vite@8.0.14(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.8.3)) + version: 4.3.0(vite@8.0.16(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.9.0)) '@vitejs/plugin-vue': specifier: ^6.0.7 - version: 6.0.7(vite@8.0.14(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.8.3))(vue@3.5.35) + version: 6.0.7(vite@8.0.16(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.9.0))(vue@3.5.35) code-inspector-plugin: specifier: ^1.5.1 - version: 1.5.1 + version: 1.5.1(supports-color@5.5.0) nodemon: specifier: ^3.1.14 version: 3.1.14 @@ -151,8 +151,8 @@ importers: specifier: ^3.15.1 version: 3.15.1 vite: - specifier: ^8.0.14 - version: 8.0.14(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.8.3) + specifier: ^8.0.16 + version: 8.0.16(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.9.0) packages: @@ -198,9 +198,9 @@ packages: resolution: {integrity: sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==} engines: {node: ^22.18.0 || >=24.11.0} - '@cloudflare/speedtest@1.9.1': - resolution: {integrity: sha512-pXJ5vXvZCh0qUZnvvpjSL+VxD9aBa5PZKF7Diysby0oY0AkHYwNIbE7dGBmXC6YCeoYScEpRdHP5wPxLxFa5KA==} - engines: {node: '>=12'} + '@cloudflare/speedtest@1.10.1': + resolution: {integrity: sha512-VRS5cONxbK82MLufBcihPzQXdYgJexj7x2cu4oIv8lSOFfSrKNM/sfzBpBYbrqPuubOY4Xm111RbM3SEzHCkwQ==} + engines: {node: '>=18'} '@code-inspector/core@1.5.1': resolution: {integrity: sha512-Y9JdgoxVh93xRMupTa1lT/v+UlcBEpM7Y1BTxQy924wSe6VVEXsJ1nPJ/Ob2HPMUAA6F568aHALi2KDUhA2kzg==} @@ -229,8 +229,8 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@firebase/ai@2.12.0': - resolution: {integrity: sha512-b+OL4vdyiSLZL/7dLd67V55CjKJvU9MpNmwnday7eA6GG2+J4iwUEsEHgw0/jKY3A41FfkF0SrnYFvtKbQZ65A==} + '@firebase/ai@2.13.0': + resolution: {integrity: sha512-nJJDQKqjAcbkZdZGT/5WTVLrGZ+pYhWbwKC90nNzmvtoRTtnOJaNS34fhKSHQeB9SALgD2kxuWT5I4AkytdZ/Q==} engines: {node: '>=20.0.0'} peerDependencies: '@firebase/app': 0.x @@ -249,8 +249,8 @@ packages: peerDependencies: '@firebase/app': 0.x - '@firebase/app-check-compat@0.4.3': - resolution: {integrity: sha512-L3AKIRTJxT9b7cDUH3OyV8gWTnmW3vYkwdzRsukWt4kbPBTct12xalnyvHDkm1lKkr+cQq/4uzBx1bOWsQ2ciw==} + '@firebase/app-check-compat@0.4.4': + resolution: {integrity: sha512-9iP0MvmaVagulNXmrca96U3tqNAI3j98wsC1z7rj62nnOTajlrHM//jjB9VoHqRw6/islMskp6RsKnM7vhLDqA==} engines: {node: '>=20.0.0'} peerDependencies: '@firebase/app-compat': 0.x @@ -261,25 +261,25 @@ packages: '@firebase/app-check-types@0.5.4': resolution: {integrity: sha512-xV7JsIyzVr15aA7f3Pi0rB9gdBuVubs89FGA8VkRYA4g0l78poADgdfrScgf7NndSg9mm7cR7PJyY0+t22KaGw==} - '@firebase/app-check@0.11.3': - resolution: {integrity: sha512-aJ4DfubWfTO8/2vhEhIAizOoOmiycESTU32e+OUgbWcS/G3PA4Vxlr/9zaiN2wfUG2AptQ7DTvj00tyuFZP5Bg==} + '@firebase/app-check@0.11.4': + resolution: {integrity: sha512-G8EsbVJV9gSfoibx0dNoNOUrvr+PkL7J//+W/BST/oUassimkZeq9bjj3bKkB0pn4og5GMQ9qs7FefwP00kkgg==} engines: {node: '>=20.0.0'} peerDependencies: '@firebase/app': 0.x - '@firebase/app-compat@0.5.12': - resolution: {integrity: sha512-Pe513OBerK/CIBxz4/za9atd5MsZtd6DzHz4cmqkvkrcDWhQChAoHBpZ3McuZNuSP8YZiKwfX/J1frR07l15/w==} + '@firebase/app-compat@0.5.13': + resolution: {integrity: sha512-pn3FvXwUR34kWPccDQfCKsNZcM2wD1OS+J1jeEgzM1ZNXoxR2NaF6e5DjDuRrnTwR6LN2XQQt0IqE6yKmgpCQg==} engines: {node: '>=20.0.0'} '@firebase/app-types@0.9.5': resolution: {integrity: sha512-YevqTjvo7Iujsa9Dwowmd6dSoElhzmD63ZSrq6bzjvQ6POjYgNjOFHLmNIgJs48eNO093NCERibuFnxbfOvU7A==} - '@firebase/app@0.14.12': - resolution: {integrity: sha512-FT+HoNp1NdaZ/N26hCwV3WbxS1m6gTn3p2QRBQ3KH7YqyCQqJx0iT7126RgVk68/Rq+9DeL/zCFnHZ0C4u1nLQ==} + '@firebase/app@0.14.13': + resolution: {integrity: sha512-H89Jeyp31+EZk9GPu6vaeL9mEmoXgM3nASB7UPBYYS/lqAks21mO1BU1dF8NbsVTL6tgGZkGUtiGJgxtDiwHkw==} engines: {node: '>=20.0.0'} - '@firebase/auth-compat@0.6.6': - resolution: {integrity: sha512-KDJ/GAf/rt7galOpn3DRb2buFfGkZCsHTryKjXDG0eeRnok4+2B4nnkMOMdjRnPkElmcJv2Ao0vEA6kp5m98PQ==} + '@firebase/auth-compat@0.6.7': + resolution: {integrity: sha512-XgKnOgY1Siq7gylAmLkYtHAlRxNeWEAspH+nO3gJZJnfHqoTHbr9UjJ3nHNFALYXV5CfpQlyPROyB2ztySBHBQ==} engines: {node: '>=20.0.0'} peerDependencies: '@firebase/app-compat': 0.x @@ -293,8 +293,8 @@ packages: '@firebase/app-types': 0.x '@firebase/util': 1.x - '@firebase/auth@1.13.1': - resolution: {integrity: sha512-/1nkKY/MicI+I9WWcx6R4NKs77AaW9NQ0IwsFdUBomWrW0/cXEmopfM2dtLm2oI1qG6z6vom3CXZDHJIJXoMuw==} + '@firebase/auth@1.13.2': + resolution: {integrity: sha512-B4w0iS7MxRg28oIh2fJFTE6cM0lYdBrW19eHpc42jqEcloUjlYyVrpPqZvqA4+v9KFEVSKEs2SfWyta7hbzkJQ==} engines: {node: '>=20.0.0'} peerDependencies: '@firebase/app': 0.x @@ -307,8 +307,8 @@ packages: resolution: {integrity: sha512-wFofIaa2879ogD/WvkjYXJxRmfnL0scen6ORgaC3na1FNOR9ASIUANQdhqQcmWu/h77/pVHY7ch5flewa5Bcew==} engines: {node: '>=20.0.0'} - '@firebase/data-connect@0.7.0': - resolution: {integrity: sha512-ar9sNOJh5poQCSMSVlnVE8eo8+usTD1POWDCv65omkKUvnFMcdXaQ7J/e7WGKqJzcEMgiezSX/TZiKHZkItMbQ==} + '@firebase/data-connect@0.7.1': + resolution: {integrity: sha512-2LbUU8mmSA63HknxQMmWHjpzuNLBKflvVwQc2tpoVKg0biWleNEJX031ELks0vzFs+dDjOUkCJR72RP6mQHFOg==} peerDependencies: '@firebase/app': 0.x @@ -323,8 +323,8 @@ packages: resolution: {integrity: sha512-XwWCa+E4TvNGpGwXrycLRNfdogADwFcvuhyow6wDWma9W54roaQIhe+4PM0KiLsIftBdSCGI7OKCXrdSRHbIhw==} engines: {node: '>=20.0.0'} - '@firebase/firestore-compat@0.4.9': - resolution: {integrity: sha512-NPtBuFr79BbIQJXFWhW4xFC6rBksK8/ewqCTYbbAYfZBDDx0/iHTUj4WpKi5D4d0Pn2Md/3T/e5V9379G5N/Zg==} + '@firebase/firestore-compat@0.4.10': + resolution: {integrity: sha512-yMP3FADDjikdrQv4YmvL4EkIny6Hw+N+a2O5T40rlHiniyMpRPxgYkKiFOvMZnsqKLqBVnKqCAElC0pa/IZtdw==} engines: {node: '>=20.0.0'} peerDependencies: '@firebase/app-compat': 0.x @@ -335,14 +335,14 @@ packages: '@firebase/app-types': 0.x '@firebase/util': 1.x - '@firebase/firestore@4.14.1': - resolution: {integrity: sha512-PouS0NJZ3NYOZE/tPDvXa8VUeJ10Ll//7jIdFvMYdhQkd/P3O7nlqhyoTmY0h8Xa9hxg+H0j6gxUytJcoZ9YOg==} + '@firebase/firestore@4.15.0': + resolution: {integrity: sha512-Fj9osqYkz2Rqr7kW3/A8BRd8CyJ7yA5K8YjhihRdyJWbL+FsELVcR6DpoCplrp1IyU+xeGgTubo1UOySXpY+EA==} engines: {node: '>=20.0.0'} peerDependencies: '@firebase/app': 0.x - '@firebase/functions-compat@0.4.4': - resolution: {integrity: sha512-Be+MwhseVf/eFAZwGrFJGok6S7cmsLrAPK8MgyM8LjM0MewTsx2n01WOOca9jio1UsCZOJ0aVyQobnINcdNuIQ==} + '@firebase/functions-compat@0.4.5': + resolution: {integrity: sha512-10qlUXGY25G5/1g9UihqksPp2po+ZqSE7LEizsrdUP7vrTmkysXxGSZCDyojSEp6mQe/ecRDdDDI+z4XRdb4wQ==} engines: {node: '>=20.0.0'} peerDependencies: '@firebase/app-compat': 0.x @@ -350,8 +350,8 @@ packages: '@firebase/functions-types@0.6.4': resolution: {integrity: sha512-zV6kgqtduR4rUAdC/ilS7kmb93XD7bEZoJDlVBZqlOw2uGGGCNBQBuleww2rr0Ulr3L9o2TDjumEt68/l1f9DQ==} - '@firebase/functions@0.13.4': - resolution: {integrity: sha512-oB5rpm2Emxn2+IS1gRelAeT/5tSZMwM/KhqC5LnJsmTNnS1ZDhD7ZMZNgCI8vchTW6PbaXIwEnpUryGuIQsNbg==} + '@firebase/functions@0.13.5': + resolution: {integrity: sha512-bWCx713f4kE/uFV7gdFOLBS7lDoiZj48MRkbAqe35gkXcCeWF4QjRNO07Jhmve7EJIoQOBczL29y2r8VRuN1kw==} engines: {node: '>=20.0.0'} peerDependencies: '@firebase/app': 0.x @@ -375,16 +375,16 @@ packages: resolution: {integrity: sha512-vZKLsqE1ABOy8OjQiE7cUTFn4gvaqlk88yp8N94Pk/sDpq61YqZGqmVFZTvOyflTwuYFcWirBdYGoJgbDaXKYQ==} engines: {node: '>=20.0.0'} - '@firebase/messaging-compat@0.2.26': - resolution: {integrity: sha512-fn0XvWOfK4tsDLSipwJUW9Cp6ahWA6z+iJHxZ0pHp9MzMSUNQx85yuxZAuI7gkGXfqs7+DqEDHyyS7jDGswrmQ==} + '@firebase/messaging-compat@0.2.27': + resolution: {integrity: sha512-JNOiu1PPgdHzEPEtoFiNxQuu0x9bm4bfETSQCpGfcTlgWkhlSK7uh7nlsjC10TQLUNgYetLmuutaYTh8aeYLVA==} peerDependencies: '@firebase/app-compat': 0.x - '@firebase/messaging-interop-types@0.2.4': - resolution: {integrity: sha512-wrzITQq+xw5LtygX7O0fu43/k9ABQ4x5H9/sR5m1SbNnhIRI5xd3+raSNJaJkYC4BUhM9A4ZNSnyR2sjhxnb2Q==} + '@firebase/messaging-interop-types@0.2.5': + resolution: {integrity: sha512-tUEKnaAP2Y/MNIqgnriPpV6e5l13Vs/+p2yrd6NGlncPJT9O3a8muYZtdnWe+IJ4fgKLHJVC79n/asxk/N5Msw==} - '@firebase/messaging@0.12.26': - resolution: {integrity: sha512-lHVTO9uLofymHVWkYeUtMddIPcmJvSzVbHRB88W6XKfxbcKF+p3QrfqKhDxremSB4NQjUla1Gwn7d9umSMmt/w==} + '@firebase/messaging@0.13.0': + resolution: {integrity: sha512-GZoo0uGRvEbszo83xcgbjJp4FpkmBEr4l8Z4hi8gl+P1Spn/MTK3HapanMzSX4yUHuTEiF5hasWRxOaz+o5sxQ==} peerDependencies: '@firebase/app': 0.x @@ -401,16 +401,16 @@ packages: peerDependencies: '@firebase/app': 0.x - '@firebase/remote-config-compat@0.2.24': - resolution: {integrity: sha512-EWZTt6fJ7YmPHodQNsSxAIDZY2x8P5kRPvXAc5CmzzBm+NyPFhODbfDsNllDXDL8jlzp50bVWjDY+BXepZS9Mg==} + '@firebase/remote-config-compat@0.2.25': + resolution: {integrity: sha512-FnA5S4IxFJAAFrCnYzWlO0FCaizlYdqhe42ygFMA+wE/mUP+w36iXzHyKj1OO1A+2gyMFjeRHyg8HhkJ6c5vRA==} peerDependencies: '@firebase/app-compat': 0.x '@firebase/remote-config-types@0.5.1': resolution: {integrity: sha512-cX/1LT6KQwkXzck2eSzeKnuvXZCyr8qaPpDcikoJs7jmI+oBOXixpDLeDtWj1U6GNMkIoXrEDNoyT2Ypcyp5/A==} - '@firebase/remote-config@0.8.3': - resolution: {integrity: sha512-ggGKAaLy9YNOvpFoQZgm5p5SiFw3ZFtwti08dojnBQmQicpThTxvG5xZMSpCTYMj2o3gM/yK9CVd2w+kZub8YA==} + '@firebase/remote-config@0.8.4': + resolution: {integrity: sha512-lslywR5lGvHWTu4z/MPoYs3UwS3CKdeY+ELXY87087VsOpBpkD+9Orra23tA9GW683arPTDOM3CM6eKmtiOO3g==} peerDependencies: '@firebase/app': 0.x @@ -535,8 +535,8 @@ packages: '@lit/reactive-element@2.1.2': resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} - '@lucide/vue@1.16.0': - resolution: {integrity: sha512-kiHx0fe49DMjQuvNaEOu9Jou6u9FwdqQ8xbZIKlD47sXrOI8g59kIeeQL42NBLI4AvO+UJsbaPX0hEK/4rHSBQ==} + '@lucide/vue@1.17.0': + resolution: {integrity: sha512-6Q1ZHgr5FbmJzKWe5BxlNdjLj2lbmuH1zwDtVzUJofX0w9UREwKgq4F4jwKqFYyyIS4Rj3FiJvDi2k6djukmmw==} peerDependencies: vue: '>=3.0.1' @@ -546,8 +546,8 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@oxc-project/types@0.132.0': - resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -582,97 +582,97 @@ packages: '@protobufjs/utf8@1.1.1': resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} - '@rolldown/binding-android-arm64@1.0.2': - resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.2': - resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.2': - resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.2': - resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': - resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.2': - resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.2': - resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.2': - resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.2': - resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.2': - resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.2': - resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.2': - resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.2': - resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.2': - resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.2': - resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -945,8 +945,8 @@ packages: resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} engines: {node: '>=20.19.0'} - ast-walker-scope@0.8.3: - resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} + ast-walker-scope@0.9.0: + resolution: {integrity: sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==} engines: {node: '>=20.19.0'} async@3.2.6: @@ -1230,8 +1230,8 @@ packages: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} - firebase@12.13.0: - resolution: {integrity: sha512-iutR8ejvAqk6qUClnsPz3U3VIjTWp243AX4cD3iifak5t56to1J29xUIQgSDDzaAqKvhshZerzSahwMQj2TlvA==} + firebase@12.14.0: + resolution: {integrity: sha512-aEZ/lniDR1hOCYpx/x/V8Nrrqq9pepKDNkqP/4WGZFC69gTv6F59Z4/54W/SUP4L/hFlrRNmWj35aweQq+IHow==} forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} @@ -1555,6 +1555,9 @@ packages: mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + mmdb-lib@3.0.2: resolution: {integrity: sha512-7e87vk0DdWT647wjcfEtWeMtjm+zVGqNohN/aeIymbUfjHQ2T4Sx5kM+1irVDBSloNC3CkGKxswdMoo8yhqTDg==} engines: {node: '>=10', npm: '>=6'} @@ -1726,8 +1729,8 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} - reka-ui@2.9.8: - resolution: {integrity: sha512-7dxaBJ6nQ0zOQZXPV45219tTEgZPstmihBLS9ABPhSiPiJ8SiF0sacfZHFaBptS0v9N4tzsevq+8MNBpE4p5JQ==} + reka-ui@2.9.9: + resolution: {integrity: sha512-/e+hdF9vP8E2kPrKR4RdgMQQsfpCr8l436Zn8GRWM3jKT9EG1lOO/UFMGBVEnrMLOVoJSjjmIFrej4tMOb+6qQ==} peerDependencies: vue: '>= 3.4.0' @@ -1738,8 +1741,8 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rolldown@1.0.2: - resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -1856,8 +1859,8 @@ packages: svg-pan-zoom@3.6.2: resolution: {integrity: sha512-JwnvRWfVKw/Xzfe6jriFyfey/lWJLq4bUh2jwoR5ChWQuQoOH8FEh1l/bEp46iHHKHEJWIyFJETbazraxNWECg==} - svgmap@2.20.0: - resolution: {integrity: sha512-xRn3N6cXb1lDMvkrFFsz1wZZ8GRcPCeV+OgBmtaspVPaRqb+KtCyBYB4m3agteW7io8sWxHXVAXkGqGKqPqVXQ==} + svgmap@2.20.1: + resolution: {integrity: sha512-gPLj88L7QsJudSOzpiYXgMXURBsdK2IUSZFmjTHHXuZ4rTL2RXPCLWVzjH69/wg+BqTFg3LgxXS2T+nnOjoRkg==} tailwind-merge@3.6.0: resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} @@ -1869,8 +1872,8 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} - tar@7.5.15: - resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==} + tar@7.5.16: + resolution: {integrity: sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==} engines: {node: '>=18'} thread-stream@4.0.0: @@ -1888,6 +1891,10 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1961,8 +1968,8 @@ packages: vconsole@3.15.1: resolution: {integrity: sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==} - vite@8.0.14: - resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==} + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2026,12 +2033,13 @@ packages: peerDependencies: vue: ^3.3.4 - vue-router@5.0.7: - resolution: {integrity: sha512-dqfk8kvRbCutmCOCj/XLDqDEYxc1wBdAOGLuVy5M93ifYMsBd5fIjfaPN4tQAbxr5IprdBDIox1gr4wYyOx/SA==} + vue-router@5.1.0: + resolution: {integrity: sha512-HAbiLzLEHQwxPgvsbOJDAwtavszEgLwri6XfyrsPECIFez8+59xc9LofWVdc/HEaSRT822lJ8H9Ns38VVond5g==} peerDependencies: '@pinia/colada': '>=0.21.2' '@vue/compiler-sfc': ^3.5.34 pinia: ^3.0.4 + vite: ^7.0.0 || ^8.0.0 vue: ^3.5.34 peerDependenciesMeta: '@pinia/colada': @@ -2040,6 +2048,8 @@ packages: optional: true pinia: optional: true + vite: + optional: true vue-sonner@2.0.9: resolution: {integrity: sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw==} @@ -2096,8 +2106,8 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} - yaml@2.8.3: - resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} hasBin: true @@ -2148,47 +2158,47 @@ snapshots: '@babel/helper-string-parser': 8.0.0-rc.6 '@babel/helper-validator-identifier': 8.0.0-rc.6 - '@cloudflare/speedtest@1.9.1': {} + '@cloudflare/speedtest@1.10.1': {} - '@code-inspector/core@1.5.1': + '@code-inspector/core@1.5.1(supports-color@5.5.0)': dependencies: '@vue/compiler-dom': 3.5.35 chalk: 4.1.2 dotenv: 16.6.1 launch-ide: 1.4.3 - portfinder: 1.0.38 + portfinder: 1.0.38(supports-color@5.5.0) transitivePeerDependencies: - supports-color '@code-inspector/esbuild@1.5.1': dependencies: - '@code-inspector/core': 1.5.1 + '@code-inspector/core': 1.5.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color '@code-inspector/mako@1.5.1': dependencies: - '@code-inspector/core': 1.5.1 + '@code-inspector/core': 1.5.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color '@code-inspector/turbopack@1.5.1': dependencies: - '@code-inspector/core': 1.5.1 + '@code-inspector/core': 1.5.1(supports-color@5.5.0) '@code-inspector/webpack': 1.5.1 transitivePeerDependencies: - supports-color '@code-inspector/vite@1.5.1': dependencies: - '@code-inspector/core': 1.5.1 + '@code-inspector/core': 1.5.1(supports-color@5.5.0) chalk: 4.1.1 transitivePeerDependencies: - supports-color '@code-inspector/webpack@1.5.1': dependencies: - '@code-inspector/core': 1.5.1 + '@code-inspector/core': 1.5.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -2208,9 +2218,9 @@ snapshots: tslib: 2.8.1 optional: true - '@firebase/ai@2.12.0(@firebase/app-types@0.9.5)(@firebase/app@0.14.12)': + '@firebase/ai@2.13.0(@firebase/app-types@0.9.5)(@firebase/app@0.14.13)': dependencies: - '@firebase/app': 0.14.12 + '@firebase/app': 0.14.13 '@firebase/app-check-interop-types': 0.3.4 '@firebase/app-types': 0.9.5 '@firebase/component': 0.7.3 @@ -2218,11 +2228,11 @@ snapshots: '@firebase/util': 1.15.1 tslib: 2.8.1 - '@firebase/analytics-compat@0.2.28(@firebase/app-compat@0.5.12)(@firebase/app@0.14.12)': + '@firebase/analytics-compat@0.2.28(@firebase/app-compat@0.5.13)(@firebase/app@0.14.13)': dependencies: - '@firebase/analytics': 0.10.22(@firebase/app@0.14.12) + '@firebase/analytics': 0.10.22(@firebase/app@0.14.13) '@firebase/analytics-types': 0.8.4 - '@firebase/app-compat': 0.5.12 + '@firebase/app-compat': 0.5.13 '@firebase/component': 0.7.3 '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2231,20 +2241,20 @@ snapshots: '@firebase/analytics-types@0.8.4': {} - '@firebase/analytics@0.10.22(@firebase/app@0.14.12)': + '@firebase/analytics@0.10.22(@firebase/app@0.14.13)': dependencies: - '@firebase/app': 0.14.12 + '@firebase/app': 0.14.13 '@firebase/component': 0.7.3 - '@firebase/installations': 0.6.22(@firebase/app@0.14.12) + '@firebase/installations': 0.6.22(@firebase/app@0.14.13) '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 tslib: 2.8.1 - '@firebase/app-check-compat@0.4.3(@firebase/app-compat@0.5.12)(@firebase/app@0.14.12)': + '@firebase/app-check-compat@0.4.4(@firebase/app-compat@0.5.13)(@firebase/app@0.14.13)': dependencies: - '@firebase/app-check': 0.11.3(@firebase/app@0.14.12) + '@firebase/app-check': 0.11.4(@firebase/app@0.14.13) '@firebase/app-check-types': 0.5.4 - '@firebase/app-compat': 0.5.12 + '@firebase/app-compat': 0.5.13 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 @@ -2256,17 +2266,17 @@ snapshots: '@firebase/app-check-types@0.5.4': {} - '@firebase/app-check@0.11.3(@firebase/app@0.14.12)': + '@firebase/app-check@0.11.4(@firebase/app@0.14.13)': dependencies: - '@firebase/app': 0.14.12 + '@firebase/app': 0.14.13 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 tslib: 2.8.1 - '@firebase/app-compat@0.5.12': + '@firebase/app-compat@0.5.13': dependencies: - '@firebase/app': 0.14.12 + '@firebase/app': 0.14.13 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 @@ -2276,7 +2286,7 @@ snapshots: dependencies: '@firebase/logger': 0.5.1 - '@firebase/app@0.14.12': + '@firebase/app@0.14.13': dependencies: '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 @@ -2284,10 +2294,10 @@ snapshots: idb: 7.1.1 tslib: 2.8.1 - '@firebase/auth-compat@0.6.6(@firebase/app-compat@0.5.12)(@firebase/app-types@0.9.5)(@firebase/app@0.14.12)': + '@firebase/auth-compat@0.6.7(@firebase/app-compat@0.5.13)(@firebase/app-types@0.9.5)(@firebase/app@0.14.13)': dependencies: - '@firebase/app-compat': 0.5.12 - '@firebase/auth': 1.13.1(@firebase/app@0.14.12) + '@firebase/app-compat': 0.5.13 + '@firebase/auth': 1.13.2(@firebase/app@0.14.13) '@firebase/auth-types': 0.13.1(@firebase/app-types@0.9.5)(@firebase/util@1.15.1) '@firebase/component': 0.7.3 '@firebase/util': 1.15.1 @@ -2304,9 +2314,9 @@ snapshots: '@firebase/app-types': 0.9.5 '@firebase/util': 1.15.1 - '@firebase/auth@1.13.1(@firebase/app@0.14.12)': + '@firebase/auth@1.13.2(@firebase/app@0.14.13)': dependencies: - '@firebase/app': 0.14.12 + '@firebase/app': 0.14.13 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 @@ -2317,9 +2327,9 @@ snapshots: '@firebase/util': 1.15.1 tslib: 2.8.1 - '@firebase/data-connect@0.7.0(@firebase/app@0.14.12)': + '@firebase/data-connect@0.7.1(@firebase/app@0.14.13)': dependencies: - '@firebase/app': 0.14.12 + '@firebase/app': 0.14.13 '@firebase/auth-interop-types': 0.2.5 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 @@ -2350,11 +2360,11 @@ snapshots: faye-websocket: 0.11.4 tslib: 2.8.1 - '@firebase/firestore-compat@0.4.9(@firebase/app-compat@0.5.12)(@firebase/app-types@0.9.5)(@firebase/app@0.14.12)': + '@firebase/firestore-compat@0.4.10(@firebase/app-compat@0.5.13)(@firebase/app-types@0.9.5)(@firebase/app@0.14.13)': dependencies: - '@firebase/app-compat': 0.5.12 + '@firebase/app-compat': 0.5.13 '@firebase/component': 0.7.3 - '@firebase/firestore': 4.14.1(@firebase/app@0.14.12) + '@firebase/firestore': 4.15.0(@firebase/app@0.14.13) '@firebase/firestore-types': 3.0.4(@firebase/app-types@0.9.5)(@firebase/util@1.15.1) '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2367,9 +2377,9 @@ snapshots: '@firebase/app-types': 0.9.5 '@firebase/util': 1.15.1 - '@firebase/firestore@4.14.1(@firebase/app@0.14.12)': + '@firebase/firestore@4.15.0(@firebase/app@0.14.13)': dependencies: - '@firebase/app': 0.14.12 + '@firebase/app': 0.14.13 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 @@ -2378,11 +2388,11 @@ snapshots: '@grpc/proto-loader': 0.7.15 tslib: 2.8.1 - '@firebase/functions-compat@0.4.4(@firebase/app-compat@0.5.12)(@firebase/app@0.14.12)': + '@firebase/functions-compat@0.4.5(@firebase/app-compat@0.5.13)(@firebase/app@0.14.13)': dependencies: - '@firebase/app-compat': 0.5.12 + '@firebase/app-compat': 0.5.13 '@firebase/component': 0.7.3 - '@firebase/functions': 0.13.4(@firebase/app@0.14.12) + '@firebase/functions': 0.13.5(@firebase/app@0.14.13) '@firebase/functions-types': 0.6.4 '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2391,21 +2401,21 @@ snapshots: '@firebase/functions-types@0.6.4': {} - '@firebase/functions@0.13.4(@firebase/app@0.14.12)': + '@firebase/functions@0.13.5(@firebase/app@0.14.13)': dependencies: - '@firebase/app': 0.14.12 + '@firebase/app': 0.14.13 '@firebase/app-check-interop-types': 0.3.4 '@firebase/auth-interop-types': 0.2.5 '@firebase/component': 0.7.3 - '@firebase/messaging-interop-types': 0.2.4 + '@firebase/messaging-interop-types': 0.2.5 '@firebase/util': 1.15.1 tslib: 2.8.1 - '@firebase/installations-compat@0.2.22(@firebase/app-compat@0.5.12)(@firebase/app-types@0.9.5)(@firebase/app@0.14.12)': + '@firebase/installations-compat@0.2.22(@firebase/app-compat@0.5.13)(@firebase/app-types@0.9.5)(@firebase/app@0.14.13)': dependencies: - '@firebase/app-compat': 0.5.12 + '@firebase/app-compat': 0.5.13 '@firebase/component': 0.7.3 - '@firebase/installations': 0.6.22(@firebase/app@0.14.12) + '@firebase/installations': 0.6.22(@firebase/app@0.14.13) '@firebase/installations-types': 0.5.4(@firebase/app-types@0.9.5) '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2417,9 +2427,9 @@ snapshots: dependencies: '@firebase/app-types': 0.9.5 - '@firebase/installations@0.6.22(@firebase/app@0.14.12)': + '@firebase/installations@0.6.22(@firebase/app@0.14.13)': dependencies: - '@firebase/app': 0.14.12 + '@firebase/app': 0.14.13 '@firebase/component': 0.7.3 '@firebase/util': 1.15.1 idb: 7.1.1 @@ -2429,34 +2439,34 @@ snapshots: dependencies: tslib: 2.8.1 - '@firebase/messaging-compat@0.2.26(@firebase/app-compat@0.5.12)(@firebase/app@0.14.12)': + '@firebase/messaging-compat@0.2.27(@firebase/app-compat@0.5.13)(@firebase/app@0.14.13)': dependencies: - '@firebase/app-compat': 0.5.12 + '@firebase/app-compat': 0.5.13 '@firebase/component': 0.7.3 - '@firebase/messaging': 0.12.26(@firebase/app@0.14.12) + '@firebase/messaging': 0.13.0(@firebase/app@0.14.13) '@firebase/util': 1.15.1 tslib: 2.8.1 transitivePeerDependencies: - '@firebase/app' - '@firebase/messaging-interop-types@0.2.4': {} + '@firebase/messaging-interop-types@0.2.5': {} - '@firebase/messaging@0.12.26(@firebase/app@0.14.12)': + '@firebase/messaging@0.13.0(@firebase/app@0.14.13)': dependencies: - '@firebase/app': 0.14.12 + '@firebase/app': 0.14.13 '@firebase/component': 0.7.3 - '@firebase/installations': 0.6.22(@firebase/app@0.14.12) - '@firebase/messaging-interop-types': 0.2.4 + '@firebase/installations': 0.6.22(@firebase/app@0.14.13) + '@firebase/messaging-interop-types': 0.2.5 '@firebase/util': 1.15.1 idb: 7.1.1 tslib: 2.8.1 - '@firebase/performance-compat@0.2.25(@firebase/app-compat@0.5.12)(@firebase/app@0.14.12)': + '@firebase/performance-compat@0.2.25(@firebase/app-compat@0.5.13)(@firebase/app@0.14.13)': dependencies: - '@firebase/app-compat': 0.5.12 + '@firebase/app-compat': 0.5.13 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 - '@firebase/performance': 0.7.12(@firebase/app@0.14.12) + '@firebase/performance': 0.7.12(@firebase/app@0.14.13) '@firebase/performance-types': 0.2.4 '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2465,22 +2475,22 @@ snapshots: '@firebase/performance-types@0.2.4': {} - '@firebase/performance@0.7.12(@firebase/app@0.14.12)': + '@firebase/performance@0.7.12(@firebase/app@0.14.13)': dependencies: - '@firebase/app': 0.14.12 + '@firebase/app': 0.14.13 '@firebase/component': 0.7.3 - '@firebase/installations': 0.6.22(@firebase/app@0.14.12) + '@firebase/installations': 0.6.22(@firebase/app@0.14.13) '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 tslib: 2.8.1 web-vitals: 4.2.4 - '@firebase/remote-config-compat@0.2.24(@firebase/app-compat@0.5.12)(@firebase/app@0.14.12)': + '@firebase/remote-config-compat@0.2.25(@firebase/app-compat@0.5.13)(@firebase/app@0.14.13)': dependencies: - '@firebase/app-compat': 0.5.12 + '@firebase/app-compat': 0.5.13 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 - '@firebase/remote-config': 0.8.3(@firebase/app@0.14.12) + '@firebase/remote-config': 0.8.4(@firebase/app@0.14.13) '@firebase/remote-config-types': 0.5.1 '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2489,20 +2499,20 @@ snapshots: '@firebase/remote-config-types@0.5.1': {} - '@firebase/remote-config@0.8.3(@firebase/app@0.14.12)': + '@firebase/remote-config@0.8.4(@firebase/app@0.14.13)': dependencies: - '@firebase/app': 0.14.12 + '@firebase/app': 0.14.13 '@firebase/component': 0.7.3 - '@firebase/installations': 0.6.22(@firebase/app@0.14.12) + '@firebase/installations': 0.6.22(@firebase/app@0.14.13) '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 tslib: 2.8.1 - '@firebase/storage-compat@0.4.3(@firebase/app-compat@0.5.12)(@firebase/app-types@0.9.5)(@firebase/app@0.14.12)': + '@firebase/storage-compat@0.4.3(@firebase/app-compat@0.5.13)(@firebase/app-types@0.9.5)(@firebase/app@0.14.13)': dependencies: - '@firebase/app-compat': 0.5.12 + '@firebase/app-compat': 0.5.13 '@firebase/component': 0.7.3 - '@firebase/storage': 0.14.3(@firebase/app@0.14.12) + '@firebase/storage': 0.14.3(@firebase/app@0.14.13) '@firebase/storage-types': 0.8.4(@firebase/app-types@0.9.5)(@firebase/util@1.15.1) '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2515,9 +2525,9 @@ snapshots: '@firebase/app-types': 0.9.5 '@firebase/util': 1.15.1 - '@firebase/storage@0.14.3(@firebase/app@0.14.12)': + '@firebase/storage@0.14.3(@firebase/app@0.14.13)': dependencies: - '@firebase/app': 0.14.12 + '@firebase/app': 0.14.13 '@firebase/component': 0.7.3 '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2639,7 +2649,7 @@ snapshots: dependencies: '@lit-labs/ssr-dom-shim': 1.5.1 - '@lucide/vue@1.16.0(vue@3.5.35)': + '@lucide/vue@1.17.0(vue@3.5.35)': dependencies: vue: 3.5.35 @@ -2650,7 +2660,7 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true - '@oxc-project/types@0.132.0': {} + '@oxc-project/types@0.133.0': {} '@pinojs/redact@0.4.0': {} @@ -2677,53 +2687,53 @@ snapshots: '@protobufjs/utf8@1.1.1': {} - '@rolldown/binding-android-arm64@1.0.2': + '@rolldown/binding-android-arm64@1.0.3': optional: true - '@rolldown/binding-darwin-arm64@1.0.2': + '@rolldown/binding-darwin-arm64@1.0.3': optional: true - '@rolldown/binding-darwin-x64@1.0.2': + '@rolldown/binding-darwin-x64@1.0.3': optional: true - '@rolldown/binding-freebsd-x64@1.0.2': + '@rolldown/binding-freebsd-x64@1.0.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.2': + '@rolldown/binding-linux-arm64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.2': + '@rolldown/binding-linux-arm64-musl@1.0.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.2': + '@rolldown/binding-linux-ppc64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.2': + '@rolldown/binding-linux-s390x-gnu@1.0.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.2': + '@rolldown/binding-linux-x64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-x64-musl@1.0.2': + '@rolldown/binding-linux-x64-musl@1.0.3': optional: true - '@rolldown/binding-openharmony-arm64@1.0.2': + '@rolldown/binding-openharmony-arm64@1.0.3': optional: true - '@rolldown/binding-wasm32-wasi@1.0.2': + '@rolldown/binding-wasm32-wasi@1.0.3': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.2': + '@rolldown/binding-win32-arm64-msvc@1.0.3': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.2': + '@rolldown/binding-win32-x64-msvc@1.0.3': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -2793,12 +2803,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 - '@tailwindcss/vite@4.3.0(vite@8.0.14(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.8.3))': + '@tailwindcss/vite@4.3.0(vite@8.0.16(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.0 '@tailwindcss/oxide': 4.3.0 tailwindcss: 4.3.0 - vite: 8.0.14(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.8.3) + vite: 8.0.16(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.9.0) '@tanstack/table-core@8.21.3': {} @@ -2841,10 +2851,10 @@ snapshots: '@types/web-bluetooth@0.0.21': {} - '@vitejs/plugin-vue@6.0.7(vite@8.0.14(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.8.3))(vue@3.5.35)': + '@vitejs/plugin-vue@6.0.7(vite@8.0.16(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.9.0))(vue@3.5.35)': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.14(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.8.3) + vite: 8.0.16(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.9.0) vue: 3.5.35 '@vue-macros/common@3.1.2(vue@3.5.35)': @@ -3005,9 +3015,10 @@ snapshots: '@babel/parser': 7.29.3 pathe: 2.0.3 - ast-walker-scope@0.8.3: + ast-walker-scope@0.9.0: dependencies: '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 ast-kit: 2.2.0 async@3.2.6: {} @@ -3022,7 +3033,7 @@ snapshots: birpc@2.8.0: {} - body-parser@2.2.1: + body-parser@2.2.1(supports-color@5.5.0): dependencies: bytes: 3.1.2 content-type: 1.0.5 @@ -3107,9 +3118,9 @@ snapshots: clsx@2.1.1: {} - code-inspector-plugin@1.5.1: + code-inspector-plugin@1.5.1(supports-color@5.5.0): dependencies: - '@code-inspector/core': 1.5.1 + '@code-inspector/core': 1.5.1(supports-color@5.5.0) '@code-inspector/esbuild': 1.5.1 '@code-inspector/mako': 1.5.1 '@code-inspector/turbopack': 1.5.1 @@ -3228,20 +3239,20 @@ snapshots: etag@1.8.1: {} - express-rate-limit@8.5.2(express@5.2.1): + express-rate-limit@8.5.2(express@5.2.1(supports-color@5.5.0)): dependencies: - express: 5.2.1 + express: 5.2.1(supports-color@5.5.0) ip-address: 10.2.0 - express-slow-down@3.1.0(express@5.2.1): + express-slow-down@3.1.0(express@5.2.1(supports-color@5.5.0)): dependencies: - express: 5.2.1 - express-rate-limit: 8.5.2(express@5.2.1) + express: 5.2.1(supports-color@5.5.0) + express-rate-limit: 8.5.2(express@5.2.1(supports-color@5.5.0)) - express@5.2.1: + express@5.2.1(supports-color@5.5.0): dependencies: accepts: 2.0.0 - body-parser: 2.2.1 + body-parser: 2.2.1(supports-color@5.5.0) content-disposition: 1.0.0 content-type: 1.0.5 cookie: 0.7.1 @@ -3251,7 +3262,7 @@ snapshots: encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.0 + finalhandler: 2.1.0(supports-color@5.5.0) fresh: 2.0.0 http-errors: 2.0.1 merge-descriptors: 2.0.0 @@ -3262,8 +3273,8 @@ snapshots: proxy-addr: 2.0.7 qs: 6.15.2 range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.0 + router: 2.2.0(supports-color@5.5.0) + send: 1.2.0(supports-color@5.5.0) serve-static: 2.2.0 statuses: 2.0.2 type-is: 2.0.1 @@ -3289,7 +3300,7 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.0: + finalhandler@2.1.0(supports-color@5.5.0): dependencies: debug: 4.4.3(supports-color@5.5.0) encodeurl: 2.0.0 @@ -3300,35 +3311,35 @@ snapshots: transitivePeerDependencies: - supports-color - firebase@12.13.0: + firebase@12.14.0: dependencies: - '@firebase/ai': 2.12.0(@firebase/app-types@0.9.5)(@firebase/app@0.14.12) - '@firebase/analytics': 0.10.22(@firebase/app@0.14.12) - '@firebase/analytics-compat': 0.2.28(@firebase/app-compat@0.5.12)(@firebase/app@0.14.12) - '@firebase/app': 0.14.12 - '@firebase/app-check': 0.11.3(@firebase/app@0.14.12) - '@firebase/app-check-compat': 0.4.3(@firebase/app-compat@0.5.12)(@firebase/app@0.14.12) - '@firebase/app-compat': 0.5.12 + '@firebase/ai': 2.13.0(@firebase/app-types@0.9.5)(@firebase/app@0.14.13) + '@firebase/analytics': 0.10.22(@firebase/app@0.14.13) + '@firebase/analytics-compat': 0.2.28(@firebase/app-compat@0.5.13)(@firebase/app@0.14.13) + '@firebase/app': 0.14.13 + '@firebase/app-check': 0.11.4(@firebase/app@0.14.13) + '@firebase/app-check-compat': 0.4.4(@firebase/app-compat@0.5.13)(@firebase/app@0.14.13) + '@firebase/app-compat': 0.5.13 '@firebase/app-types': 0.9.5 - '@firebase/auth': 1.13.1(@firebase/app@0.14.12) - '@firebase/auth-compat': 0.6.6(@firebase/app-compat@0.5.12)(@firebase/app-types@0.9.5)(@firebase/app@0.14.12) - '@firebase/data-connect': 0.7.0(@firebase/app@0.14.12) + '@firebase/auth': 1.13.2(@firebase/app@0.14.13) + '@firebase/auth-compat': 0.6.7(@firebase/app-compat@0.5.13)(@firebase/app-types@0.9.5)(@firebase/app@0.14.13) + '@firebase/data-connect': 0.7.1(@firebase/app@0.14.13) '@firebase/database': 1.1.3 '@firebase/database-compat': 2.1.4 - '@firebase/firestore': 4.14.1(@firebase/app@0.14.12) - '@firebase/firestore-compat': 0.4.9(@firebase/app-compat@0.5.12)(@firebase/app-types@0.9.5)(@firebase/app@0.14.12) - '@firebase/functions': 0.13.4(@firebase/app@0.14.12) - '@firebase/functions-compat': 0.4.4(@firebase/app-compat@0.5.12)(@firebase/app@0.14.12) - '@firebase/installations': 0.6.22(@firebase/app@0.14.12) - '@firebase/installations-compat': 0.2.22(@firebase/app-compat@0.5.12)(@firebase/app-types@0.9.5)(@firebase/app@0.14.12) - '@firebase/messaging': 0.12.26(@firebase/app@0.14.12) - '@firebase/messaging-compat': 0.2.26(@firebase/app-compat@0.5.12)(@firebase/app@0.14.12) - '@firebase/performance': 0.7.12(@firebase/app@0.14.12) - '@firebase/performance-compat': 0.2.25(@firebase/app-compat@0.5.12)(@firebase/app@0.14.12) - '@firebase/remote-config': 0.8.3(@firebase/app@0.14.12) - '@firebase/remote-config-compat': 0.2.24(@firebase/app-compat@0.5.12)(@firebase/app@0.14.12) - '@firebase/storage': 0.14.3(@firebase/app@0.14.12) - '@firebase/storage-compat': 0.4.3(@firebase/app-compat@0.5.12)(@firebase/app-types@0.9.5)(@firebase/app@0.14.12) + '@firebase/firestore': 4.15.0(@firebase/app@0.14.13) + '@firebase/firestore-compat': 0.4.10(@firebase/app-compat@0.5.13)(@firebase/app-types@0.9.5)(@firebase/app@0.14.13) + '@firebase/functions': 0.13.5(@firebase/app@0.14.13) + '@firebase/functions-compat': 0.4.5(@firebase/app-compat@0.5.13)(@firebase/app@0.14.13) + '@firebase/installations': 0.6.22(@firebase/app@0.14.13) + '@firebase/installations-compat': 0.2.22(@firebase/app-compat@0.5.13)(@firebase/app-types@0.9.5)(@firebase/app@0.14.13) + '@firebase/messaging': 0.13.0(@firebase/app@0.14.13) + '@firebase/messaging-compat': 0.2.27(@firebase/app-compat@0.5.13)(@firebase/app@0.14.13) + '@firebase/performance': 0.7.12(@firebase/app@0.14.13) + '@firebase/performance-compat': 0.2.25(@firebase/app-compat@0.5.13)(@firebase/app@0.14.13) + '@firebase/remote-config': 0.8.4(@firebase/app@0.14.13) + '@firebase/remote-config-compat': 0.2.25(@firebase/app-compat@0.5.13)(@firebase/app@0.14.13) + '@firebase/storage': 0.14.3(@firebase/app@0.14.13) + '@firebase/storage-compat': 0.4.3(@firebase/app-compat@0.5.13)(@firebase/app-types@0.9.5)(@firebase/app@0.14.13) '@firebase/util': 1.15.1 transitivePeerDependencies: - '@react-native-async-storage/async-storage' @@ -3400,7 +3411,7 @@ snapshots: http-parser-js@0.5.10: {} - http-proxy-middleware@4.0.0: + http-proxy-middleware@4.0.0(supports-color@5.5.0): dependencies: debug: 4.4.3(supports-color@5.5.0) httpxy: 0.5.1 @@ -3606,6 +3617,13 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + mmdb-lib@3.0.2: {} ms@2.1.3: {} @@ -3723,7 +3741,7 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 - portfinder@1.0.38: + portfinder@1.0.38(supports-color@5.5.0): dependencies: async: 3.2.6 debug: 4.4.3(supports-color@5.5.0) @@ -3794,7 +3812,7 @@ snapshots: real-require@0.2.0: {} - reka-ui@2.9.8(vue@3.5.35): + reka-ui@2.9.9(vue@3.5.35): dependencies: '@floating-ui/dom': 1.7.6 '@floating-ui/vue': 1.1.11(vue@3.5.35) @@ -3814,28 +3832,28 @@ snapshots: rfdc@1.4.1: {} - rolldown@1.0.2: + rolldown@1.0.3: dependencies: - '@oxc-project/types': 0.132.0 + '@oxc-project/types': 0.133.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.2 - '@rolldown/binding-darwin-arm64': 1.0.2 - '@rolldown/binding-darwin-x64': 1.0.2 - '@rolldown/binding-freebsd-x64': 1.0.2 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 - '@rolldown/binding-linux-arm64-gnu': 1.0.2 - '@rolldown/binding-linux-arm64-musl': 1.0.2 - '@rolldown/binding-linux-ppc64-gnu': 1.0.2 - '@rolldown/binding-linux-s390x-gnu': 1.0.2 - '@rolldown/binding-linux-x64-gnu': 1.0.2 - '@rolldown/binding-linux-x64-musl': 1.0.2 - '@rolldown/binding-openharmony-arm64': 1.0.2 - '@rolldown/binding-wasm32-wasi': 1.0.2 - '@rolldown/binding-win32-arm64-msvc': 1.0.2 - '@rolldown/binding-win32-x64-msvc': 1.0.2 - - router@2.2.0: + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 + + router@2.2.0(supports-color@5.5.0): dependencies: debug: 4.4.3(supports-color@5.5.0) depd: 2.0.0 @@ -3861,7 +3879,7 @@ snapshots: semver@7.6.3: {} - send@1.2.0: + send@1.2.0(supports-color@5.5.0): dependencies: debug: 4.4.3(supports-color@5.5.0) encodeurl: 2.0.0 @@ -3882,7 +3900,7 @@ snapshots: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.0 + send: 1.2.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -3964,7 +3982,7 @@ snapshots: svg-pan-zoom@3.6.2: {} - svgmap@2.20.0: + svgmap@2.20.1: dependencies: svg-pan-zoom: 3.6.2 @@ -3974,7 +3992,7 @@ snapshots: tapable@2.3.3: {} - tar@7.5.15: + tar@7.5.16: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -3995,6 +4013,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -4051,10 +4074,10 @@ snapshots: vary@1.1.2: {} - vaul-vue@0.4.1(reka-ui@2.9.8(vue@3.5.35))(vue@3.5.35): + vaul-vue@0.4.1(reka-ui@2.9.9(vue@3.5.35))(vue@3.5.35): dependencies: '@vueuse/core': 10.11.1(vue@3.5.35) - reka-ui: 2.9.8(vue@3.5.35) + reka-ui: 2.9.9(vue@3.5.35) vue: 3.5.35 transitivePeerDependencies: - '@vue/composition-api' @@ -4066,18 +4089,18 @@ snapshots: core-js: 3.49.0 mutation-observer: 1.0.3 - vite@8.0.14(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.8.3): + vite@8.0.16(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.15 - rolldown: 1.0.2 - tinyglobby: 0.2.16 + rolldown: 1.0.3 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.1.0 fsevents: 2.3.3 jiti: 2.6.1 - yaml: 2.8.3 + yaml: 2.9.0 vue-demi@0.14.10(vue@3.5.35): dependencies: @@ -4096,17 +4119,17 @@ snapshots: markdown-it: 14.1.1 vue: 3.5.35 - vue-router@5.0.7(@vue/compiler-sfc@3.5.35)(pinia@3.0.4(vue@3.5.35))(vue@3.5.35): + vue-router@5.1.0(@vue/compiler-sfc@3.5.35)(pinia@3.0.4(vue@3.5.35))(vite@8.0.16(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.9.0))(vue@3.5.35): dependencies: '@babel/generator': 8.0.0-rc.6 '@vue-macros/common': 3.1.2(vue@3.5.35) '@vue/devtools-api': 8.1.2 - ast-walker-scope: 0.8.3 + ast-walker-scope: 0.9.0 chokidar: 5.0.0 json5: 2.2.3 local-pkg: 1.1.2 magic-string: 0.30.21 - mlly: 1.8.0 + mlly: 1.8.2 muggle-string: 0.4.1 pathe: 2.0.3 picomatch: 4.0.4 @@ -4115,10 +4138,11 @@ snapshots: unplugin: 3.0.0 unplugin-utils: 0.3.1 vue: 3.5.35 - yaml: 2.8.3 + yaml: 2.9.0 optionalDependencies: '@vue/compiler-sfc': 3.5.35 pinia: 3.0.4(vue@3.5.35) + vite: 8.0.16(@types/node@24.1.0)(jiti@2.6.1)(yaml@2.9.0) vue-sonner@2.0.9: {} @@ -4158,7 +4182,7 @@ snapshots: yallist@5.0.0: {} - yaml@2.8.3: {} + yaml@2.9.0: {} yargs-parser@21.1.1: {} From 52f708bd6813ad65b71b3a67b2a0df1d4d866f6d Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sat, 6 Jun 2026 14:30:59 +0800 Subject: [PATCH 02/35] Feat(service-status): add Service Status homepage section Adds a "Service Status" section between Connectivity and WebRTC showing the live availability of well-known products (Claude, OpenAI, Cursor, GitHub, Discord, Cloudflare, Reddit, Notion) from their Statuspage / incident.io pages. Backend - common/service-status-providers.js: upstream list + id whitelist - common/service-status-transform.js: pure summary/incidents normalizers (first-level components only; incident URLs built from page + id) - common/service-status-store.js: 5-min in-memory poller with last-known-good so transient upstream blips don't degrade a healthy provider - api/service-status.js: overview + per-provider components/incidents, served from the in-memory snapshot (no upstream call per request) - guards.js: requireValidProviderId; routes edge-cached for 5 min Frontend - ServiceStatus.vue: static provider list (cards always render), expandable cards with Services / Recent Incidents tabs, skeleton placeholders, ri brand icons, status-icon tones; incidents show title + date / severity (! count) / lifecycle-colored status - utils/service-status-tone.js: status-vocab -> tone/level helpers - wired into sections.js, App.vue, refresh orchestrator; i18n in en/zh/fr/tr Tests: transform/tone unit tests, guard + handler smokes; changelog entry. Co-Authored-By: Claude Opus 4.8 --- api/AGENTS.md | 9 +- api/service-status.js | 44 +++ backend-server.js | 13 +- common/guards.js | 15 + common/service-status-providers.js | 32 ++ common/service-status-store.js | 116 ++++++ common/service-status-transform.js | 75 ++++ frontend/App.vue | 5 +- frontend/components/Advanced.vue | 2 +- frontend/components/ConnectivityTest.vue | 2 +- frontend/components/ServiceStatus.vue | 359 ++++++++++++++++++ .../composables/use-refresh-orchestrator.js | 7 +- frontend/data/changelog.json | 15 + frontend/data/sections.js | 1 + frontend/locales/en.json | 40 ++ frontend/locales/fr.json | 40 ++ frontend/locales/tr.json | 40 ++ frontend/locales/zh.json | 40 ++ frontend/utils/service-status-tone.js | 89 +++++ tests/api-handlers.test.js | 50 +++ tests/composable-refresh-orchestrator.test.js | 14 +- tests/guards.test.js | 30 +- tests/sections.test.js | 6 +- tests/service-status-transform.test.js | 161 ++++++++ 24 files changed, 1189 insertions(+), 16 deletions(-) create mode 100644 api/service-status.js create mode 100644 common/service-status-providers.js create mode 100644 common/service-status-store.js create mode 100644 common/service-status-transform.js create mode 100644 frontend/components/ServiceStatus.vue create mode 100644 frontend/utils/service-status-tone.js create mode 100644 tests/service-status-transform.test.js diff --git a/api/AGENTS.md b/api/AGENTS.md index 768de800f..b0604032d 100644 --- a/api/AGENTS.md +++ b/api/AGENTS.md @@ -25,6 +25,9 @@ api/ ├── get-whois.js ← /api/whois — whoiser primary + RDAP fallback for new gTLDs ├── cf-radar.js ← /api/cfradar — ASN details via Cloudflare Radar ├── dns-resolver.js ← /api/dnsresolver — DNS + DoH parallel query +├── service-status.js ← /api/service-status (+ /components, /incidents) — +│ serves the in-memory snapshot from the poller +│ (common/service-status-store.js); no upstream call per request ├── dns-leak-test.js ← /api/dnsleaktest/session/:token — proxy to private │ IPCheck.ing endpoint (Firebase-gated) that drives the │ in-depth DNS Leak Test advanced tool @@ -33,11 +36,14 @@ api/ common/ ├── fetch-with-timeout.js ← fetchWithTimeout (5s default) + fetchUpstream (8s preset) -├── guards.js ← requireReferer + requireValidIP Express middleware +├── guards.js ← requireReferer + requireValidIP / Prefix / ASN / ProviderId middleware ├── logger.js ← shared pino logger (pretty in dev, JSON in prod) ├── referer-check.js ← low-level referer allow-list check ├── valid-ip.js ← IPv4 / IPv6 validator (also re-exported from frontend) ├── rdap.js ← RDAP client (domain fallback when whoiser returns no __raw) +├── service-status-providers.js ← static upstream list + id whitelist for service-status +├── service-status-transform.js ← pure summary / incidents normalizers (unit-tested) +├── service-status-store.js ← 5-min in-memory poller + cache behind /api/service-status ├── maxmind-service.js ← mmdb reader + lookup ├── maxmind-updater.js ← mmdb bootstrap download at boot + │ scheduled auto-update @@ -70,6 +76,7 @@ common/ - `requireValidIP()` is attached per-route to every handler that takes `?ip=`. It rejects missing or malformed IPs before the handler runs. **Handlers must not repeat the IP check** — inside the handler body, `req.query.ip` is already known to be a well-formed string. - `requireValidPrefix()` is the same pattern for `?prefix=` (CIDR-shaped param). Used by `asn-history` so the frontend can quantize the user's IP to its BGP DFZ-floor (/24 v4 or /48 v6) before the request lands, maximizing CF edge cache reuse across every IP in the same prefix. - `requireValidASN()` does the same for `?asn=` (numeric, with optional leading `AS`). Strips the prefix and rewrites `req.query.asn` to a pure numeric string. Used by `asn-connectivity`; older handlers (`cf-radar`) still validate inline. +- `requireValidProviderId()` whitelists `?id=` against the `service-status` provider slugs. Used by the `/api/service-status/components` and `/incidents` detail routes so an `id` can only select a known provider's slice of the in-memory snapshot. - If you add a new handler that needs a different-shape param guard, add the guard to `common/guards.js` and attach it in `backend-server.js` rather than open-coding the check in the handler. ### Private-API header pass-through (intentional exception) diff --git a/api/service-status.js b/api/service-status.js new file mode 100644 index 000000000..00028624a --- /dev/null +++ b/api/service-status.js @@ -0,0 +1,44 @@ +// /api/service-status — the "Service Status" section's read endpoints. +// +// All three handlers below serve slices of the in-memory snapshot maintained +// by common/service-status-store.js (a background timer refreshes every +// provider on a fixed 5-minute schedule). None of them touch an upstream — +// request volume has no effect on upstream load. The split into overview vs +// per-provider detail keeps the initial page load light: the components / +// incidents lists are only fetched when a user expands a provider card. +// +// default GET /api/service-status → overview (light per provider) +// componentsHandler GET /api/service-status/components → one provider's sub-services +// incidentsHandler GET /api/service-status/incidents → one provider's recent incidents +// +// The detail handlers' `id` query param is whitelisted by +// requireValidProviderId() in backend-server.js. + +import { getServiceStatusOverview, getProviderDetail } from '../common/service-status-store.js'; + +// Overview: every provider's status light, no heavy detail arrays. +export default async (req, res) => { + res.json(getServiceStatusOverview()); +}; + +// One provider's first-level sub-service list. Loaded when a card expands. +export const componentsHandler = async (req, res) => { + // Defensive method gate — the route already restricts to GET, but a smoke + // test asserts on this branch directly against the handler. + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + const id = req.query.id; + const provider = getProviderDetail(id); + res.json({ id, components: provider ? provider.components : [] }); +}; + +// One provider's recent incident history. Loaded on the incidents tab. +export const incidentsHandler = async (req, res) => { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + const id = req.query.id; + const provider = getProviderDetail(id); + res.json({ id, incidents: provider ? provider.incidents : [] }); +}; diff --git a/backend-server.js b/backend-server.js index 8f99289b4..28e7b85c8 100644 --- a/backend-server.js +++ b/backend-server.js @@ -7,7 +7,7 @@ import { slowDown } from 'express-slow-down' import rateLimit from 'express-rate-limit'; import pinoHttp from 'pino-http'; import logger from './common/logger.js'; -import { requireReferer, requireValidIP, requireValidPrefix, requireValidASN } from './common/guards.js'; +import { requireReferer, requireValidIP, requireValidPrefix, requireValidASN, requireValidProviderId } from './common/guards.js'; // Backend APIs import mapHandler from './api/google-map.js'; @@ -24,6 +24,10 @@ import cfHander from './api/cf-radar.js'; import asnHistoryHandler from './api/asn-history.js'; import asnConnectivityHandler from './api/asn-connectivity.js'; import dnsResolver from './api/dns-resolver.js'; +import serviceStatusHandler, { + componentsHandler as serviceStatusComponentsHandler, + incidentsHandler as serviceStatusIncidentsHandler, +} from './api/service-status.js'; import { getSessionResult as dnsLeakGetResult } from './api/dns-leak-test.js'; import getWhois from './api/get-whois.js'; import invisibilitytestHandler from './api/invisibility-test.js'; @@ -35,6 +39,7 @@ import updateUserAchievement from './api/update-user-achievement.js'; import { reloadMaxMindDatabases, startMaxMindFileWatcher } from './common/maxmind-service.js'; import { startMaxMindAutoUpdate, bootstrapMaxMindIfMissing } from './common/maxmind-updater.js'; import { startCaidaAutoUpdate, bootstrapCaidaIfMissing } from './common/caida-updater.js'; +import { bootstrapServiceStatus, startServiceStatusPolling } from './common/service-status-store.js'; dotenv.config({ quiet: true }); @@ -188,6 +193,7 @@ const cacheable = (maxAgeSeconds) => (req, res, next) => { // check individually — see common/guards.js. app.use('/api', requireReferer); +const FIVE_MIN_CACHE = 5 * 60; const ONE_HOUR_CACHE = 60 * 60; const ONE_DAY_CACHE = 24 * 60 * 60; const ONE_WEEK_CACHE = 7 * 24 * 60 * 60; @@ -205,6 +211,9 @@ app.get('/api/ipapiis', requireValidIP(), cacheable(ONE_HOUR_CACHE), ipapiisHand app.get('/api/ip2location', requireValidIP(), cacheable(ONE_HOUR_CACHE), ip2locationHandler); app.get('/api/macchecker', cacheable(THIRTY_DAYS_CACHE), macChecker); app.get('/api/maxmind', requireValidIP(), cacheable(ONE_DAY_CACHE), maxmindHandler); +app.get('/api/service-status', cacheable(FIVE_MIN_CACHE), serviceStatusHandler); +app.get('/api/service-status/components', requireValidProviderId(), cacheable(FIVE_MIN_CACHE), serviceStatusComponentsHandler); +app.get('/api/service-status/incidents', requireValidProviderId(), cacheable(FIVE_MIN_CACHE), serviceStatusIncidentsHandler); // Non-cacheable routes — auth-context, debug tools, or per-request lookups. app.get('/api/map', mapHandler); @@ -232,10 +241,12 @@ async function bootBackend() { logger.error('❌ MaxMind API will return 503 until databases are loaded successfully'); }); await bootstrapCaidaIfMissing(); + await bootstrapServiceStatus(); startMaxMindFileWatcher(); startMaxMindAutoUpdate({ reload: reloadMaxMindDatabases }); startCaidaAutoUpdate(); + startServiceStatusPolling(); app.listen(backEndPort, () => { logger.info(`🚀 Backend server ready on http://localhost:${backEndPort}`); diff --git a/common/guards.js b/common/guards.js index 00c04aaa1..e3513eaa2 100644 --- a/common/guards.js +++ b/common/guards.js @@ -6,6 +6,7 @@ import { refererCheck } from './referer-check.js'; import { isValidIP } from './valid-ip.js'; import { isValidBgpPrefix } from './bgp-prefix.js'; +import { STATUS_PROVIDER_IDS } from './service-status-providers.js'; // Reject requests without an allowed referer. The error message variant // preserves the existing user-facing wording. @@ -62,3 +63,17 @@ export const requireValidASN = (paramName = 'asn') => (req, res, next) => { req.query[paramName] = numeric; next(); }; + +// Reject requests whose `id` isn't a known service-status provider slug. +// Used by the per-provider components / incidents endpoints, which select a +// row from the in-memory snapshot by id. +export const requireValidProviderId = (paramName = 'id') => (req, res, next) => { + const id = req.query[paramName]; + if (!id) { + return res.status(400).json({ error: 'No provider id provided' }); + } + if (!STATUS_PROVIDER_IDS.has(id)) { + return res.status(400).json({ error: 'Invalid provider id' }); + } + next(); +}; diff --git a/common/service-status-providers.js b/common/service-status-providers.js new file mode 100644 index 000000000..b054cb8b8 --- /dev/null +++ b/common/service-status-providers.js @@ -0,0 +1,32 @@ +// Upstream source-of-truth for the "Service Status" homepage section. +// +// Each provider exposes a status page that is API-compatible with the Atlassian +// Statuspage / incident.io schema — both expose the same +// `/api/v2/summary.json` + `/api/v2/incidents.json` endpoints. Cloudflare +// self-hosts a page that mirrors the same shape. +// +// `id` — stable slug +// `name` — display name +// `api` — origin we hit `${api}/api/v2/{summary,incidents}.json` against +// `page` — public status page; also the base for incident detail links +// (`${page}/incidents/${incidentId}`), since incident.io's feed omits +// a usable shortlink. +// +// This list lives backend-side: the poller (common/service-status-store.js) +// walks it on a fixed schedule and caches the result in memory, so the +// upstream origins never reach the shipped frontend bundle. + +export const STATUS_PROVIDERS = [ + { id: 'claude', name: 'Claude', api: 'https://status.claude.com', page: 'https://status.claude.com' }, + { id: 'openai', name: 'OpenAI', api: 'https://status.openai.com', page: 'https://status.openai.com' }, + { id: 'cursor', name: 'Cursor', api: 'https://status.cursor.com', page: 'https://status.cursor.com' }, + { id: 'github', name: 'GitHub', api: 'https://www.githubstatus.com', page: 'https://www.githubstatus.com' }, + { id: 'discord', name: 'Discord', api: 'https://discordstatus.com', page: 'https://discordstatus.com' }, + { id: 'cloudflare', name: 'Cloudflare', api: 'https://new.cloudflarestatus.com', page: 'https://new.cloudflarestatus.com' }, + { id: 'reddit', name: 'Reddit', api: 'https://www.redditstatus.com', page: 'https://www.redditstatus.com' }, + { id: 'notion', name: 'Notion', api: 'https://www.notion-status.com', page: 'https://www.notion-status.com' }, +]; + +// Whitelist used by requireValidProviderId() — the per-provider detail +// endpoints (components / incidents) take an `id` query param. +export const STATUS_PROVIDER_IDS = new Set(STATUS_PROVIDERS.map((p) => p.id)); diff --git a/common/service-status-store.js b/common/service-status-store.js new file mode 100644 index 000000000..c766028fb --- /dev/null +++ b/common/service-status-store.js @@ -0,0 +1,116 @@ +// In-memory poller + cache for the "Service Status" section. +// +// Rather than fetch upstream status pages on each visitor request, a single +// background timer refreshes all providers every REFRESH_INTERVAL and keeps +// the latest normalized snapshot in memory. Request handlers just read the +// snapshot — upstream load is constant (8 providers × 2 endpoints / 5 min) +// no matter how much traffic the site gets. +// +// Mirrors the bootstrap-at-boot + start-scheduler shape of the MaxMind / CAIDA +// updaters, but kept deliberately lean: memory only, no files, no locks. + +import { fetchUpstream } from './fetch-with-timeout.js'; +import logger from './logger.js'; +import { STATUS_PROVIDERS } from './service-status-providers.js'; +import { assembleProvider } from './service-status-transform.js'; + +const REFRESH_INTERVAL_MS = 5 * 60 * 1000; + +// Latest snapshot served to clients. `updatedAt` stays null until the first +// successful-or-degraded refresh lands. +let snapshot = { updatedAt: null, providers: [] }; +let schedulerStarted = false; + +// Fetch + parse one JSON endpoint; throws on non-2xx so the caller can degrade. +async function fetchJson(url) { + const res = await fetchUpstream(url); + if (!res.ok) throw new Error(`upstream ${res.status}`); + return res.json(); +} + +// Refresh a single provider. Never throws. +// +// `previous` is this provider's last published entry (if any). On a failed +// summary fetch we serve last-known-good rather than flipping a healthy +// provider to 'unknown' — upstream status pages have frequent transient +// blips (DNS hiccups, TLS resets) that shouldn't surface as "status +// unavailable". Only the genuinely-degraded case (no prior good data) is a +// warn; recoverable blips drop to debug to keep the prod log quiet. +async function refreshProvider(provider, previous) { + const [summary, incidents] = await Promise.allSettled([ + fetchJson(`${provider.api}/api/v2/summary.json`), + fetchJson(`${provider.api}/api/v2/incidents.json`), + ]); + const summaryJson = summary.status === 'fulfilled' ? summary.value : null; + const incidentsJson = incidents.status === 'fulfilled' ? incidents.value : null; + + if (!summaryJson) { + const hadGood = previous && previous.indicator !== 'unknown'; + if (hadGood) { + logger.debug({ provider: provider.id }, 'service-status refresh blip; serving last-known-good'); + return previous; + } + logger.warn({ err: summary.reason, provider: provider.id }, 'service-status summary fetch failed (no prior data)'); + } + + const entry = assembleProvider(provider, summaryJson, incidentsJson); + // Keep prior incidents if only the incidents endpoint blipped this tick. + if (!incidentsJson && previous?.incidents?.length) { + entry.incidents = previous.incidents; + } + return entry; +} + +// Refresh every provider in parallel and publish a new snapshot. Exported so +// boot can await an initial fill before the server starts serving. +export async function refreshServiceStatus() { + const prevById = new Map(snapshot.providers.map((p) => [p.id, p])); + const providers = await Promise.all( + STATUS_PROVIDERS.map((p) => refreshProvider(p, prevById.get(p.id))), + ); + snapshot = { updatedAt: new Date().toISOString(), providers }; + return snapshot; +} + +// Lightweight overview: every provider's status light, without the heavier +// components / incidents arrays. Served by /api/service-status so the initial +// page load stays small; detail is pulled per-provider on demand. +export function getServiceStatusOverview() { + return { + updatedAt: snapshot.updatedAt, + providers: snapshot.providers.map(({ id, name, page, indicator }) => ({ + id, name, page, indicator, + })), + }; +} + +// One provider's full cached entry (or null if not in the snapshot yet). +// Backs the components / incidents detail endpoints. +export function getProviderDetail(id) { + return snapshot.providers.find((p) => p.id === id) || null; +} + +// Populate the cache once at boot. Non-fatal: a failure leaves an empty +// snapshot that the next scheduled tick will fill. +export async function bootstrapServiceStatus() { + try { + await refreshServiceStatus(); + logger.info('📦 Service status cache primed'); + } catch (error) { + logger.warn({ err: error }, '⚠️ Service status initial refresh failed; will retry on schedule'); + } +} + +// Start the 5-minute refresh loop. Idempotent; timer is unref'd so it never +// keeps the process alive on its own. +export function startServiceStatusPolling() { + if (schedulerStarted) return; + schedulerStarted = true; + const tick = () => { + refreshServiceStatus().catch((error) => { + logger.error({ err: error }, 'service-status refresh tick failed'); + }); + }; + setInterval(tick, REFRESH_INTERVAL_MS).unref?.(); + logger.info(`🗓️ Service status auto refresh every ${REFRESH_INTERVAL_MS / 60000} minutes (${STATUS_PROVIDERS.length} providers)`); +} diff --git a/common/service-status-transform.js b/common/service-status-transform.js new file mode 100644 index 000000000..f088a71ca --- /dev/null +++ b/common/service-status-transform.js @@ -0,0 +1,75 @@ +// Pure, network-free normalizers for the Statuspage / incident.io schema. +// +// Both `summary.json` and `incidents.json` share the same shape across every +// provider we track, so a single pair of transforms covers all of them. Kept +// dependency-free and side-effect-free so they're unit-testable with inline +// fixtures (tests/service-status-transform.test.js) — no live upstream calls. + +// Safety cap on forwarded components. First-level lists are short, so this is +// just a guard against a pathological page bloating the payload. +const DEFAULT_COMPONENT_LIMIT = 40; + +// Normalize a `/api/v2/summary.json` payload to +// { indicator, components: [{ name, status }] } +// where indicator is 'none' | 'minor' | 'major' | 'critical' | 'maintenance' | 'unknown'. +// +// Keeps only the *first level* of components — rows with no parent group +// (`group_id` absent). For pages that nest (Discord, Cloudflare) this keeps the +// category headers, which carry a rolled-up status, and drops their nested +// children; deeper detail belongs on the provider's own status page. Flat pages +// (Claude, GitHub, …) have no groups, so every row is kept. +export function normalizeSummary(json, limit = DEFAULT_COMPONENT_LIMIT) { + const rawComponents = Array.isArray(json?.components) ? json.components : []; + const components = rawComponents + .filter((c) => !c?.group_id) + .slice(0, limit) + .map((c) => ({ name: c?.name ?? '', status: c?.status ?? 'operational' })); + + return { + indicator: json?.status?.indicator || 'unknown', + components, + }; +} + +// Normalize a `/api/v2/incidents.json` payload to the most recent `limit` +// incidents. Upstream already returns them newest-first. +// +// `pageUrl` is the provider's status-page origin; each incident's detail link +// is built as `${pageUrl}/incidents/${id}`. We construct it rather than trust +// the feed's `shortlink` because incident.io pages (OpenAI, Notion) omit it — +// their feed otherwise points only at the page root, not the incident. +// +// Returns: [{ id, name, status, impact, startedAt, url }] +export function normalizeIncidents(json, { limit = 10, pageUrl = '' } = {}) { + const base = String(pageUrl).replace(/\/+$/, ''); // trim trailing slash(es) + const incidents = Array.isArray(json?.incidents) ? json.incidents : []; + return incidents.slice(0, limit).map((i) => { + const id = i?.id ?? null; + return { + id, + name: i?.name ?? '', + status: i?.status ?? null, + impact: i?.impact ?? 'none', + // `started_at` is the canonical start; fall back to created_at. + startedAt: i?.started_at ?? i?.created_at ?? null, + url: id && base ? `${base}/incidents/${id}` : (base || null), + }; + }); +} + +// Combine one provider's config with its (possibly null) summary + incidents +// payloads into the shape the frontend consumes. Pure: a null payload (failed +// fetch) degrades gracefully — normalizeSummary(null) → indicator 'unknown' +// with no components, normalizeIncidents(null) → []. Keeps the poller's +// per-provider error handling free of shape-building logic. +export function assembleProvider(provider, summaryJson, incidentsJson) { + const { indicator, components } = normalizeSummary(summaryJson); + return { + id: provider.id, + name: provider.name, + page: provider.page, + indicator, + components, + incidents: normalizeIncidents(incidentsJson, { pageUrl: provider.page }), + }; +} diff --git a/frontend/App.vue b/frontend/App.vue index 5eef63560..5894c8cbc 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -9,6 +9,7 @@
+ @@ -32,6 +33,7 @@ import NavBar from './components/Nav.vue'; import IPCheck from './components/IpInfos.vue'; import Connectivity from './components/ConnectivityTest.vue'; +import ServiceStatus from './components/ServiceStatus.vue'; import WebRTC from './components/WebRtcTest.vue'; import DNSLeaks from './components/DnsLeaksTest.vue'; import SpeedTest from './components/SpeedTest.vue'; @@ -85,6 +87,7 @@ const speedTestRef = ref(null); const advancedToolsRef = ref(null); const IPCheckRef = ref(null); const connectivityRef = ref(null); +const serviceStatusRef = ref(null); const webRTCRef = ref(null); const dnsLeaksRef = ref(null); @@ -120,7 +123,7 @@ const { infoMaskLevel, isInfosLoaded, showMaskButton, toggleInfoMask } = useInfo // Refresh / initial load sequence const { loadingControl } = useRefreshOrchestrator({ - refs: { IPCheckRef, connectivityRef, webRTCRef, dnsLeaksRef }, + refs: { IPCheckRef, connectivityRef, serviceStatusRef, webRTCRef, dnsLeaksRef }, store, t, userPreferences, diff --git a/frontend/components/Advanced.vue b/frontend/components/Advanced.vue index 15a6b3b7d..7a7ffde2c 100644 --- a/frontend/components/Advanced.vue +++ b/frontend/components/Advanced.vue @@ -91,7 +91,7 @@ const router = useRouter(); const cards = reactive([ { path: '/pingtest', icon: '⏱️', titleKey: 'pingtest.Title', noteKey: 'advancedtools.PingTestNote', enabled: true }, - { path: '/mtrtest', icon: '📡', titleKey: 'mtrtest.Title', noteKey: 'advancedtools.MTRTestNote', enabled: true }, + { path: '/mtrtest', icon: '🚉', titleKey: 'mtrtest.Title', noteKey: 'advancedtools.MTRTestNote', enabled: true }, { path: '/ruletest', icon: '🚏', titleKey: 'ruletest.Title', noteKey: 'advancedtools.RuleTestNote', enabled: true }, { path: '/dnsresolver', icon: '📟', titleKey: 'dnsresolver.Title', noteKey: 'advancedtools.DNSResolverNote', enabled: true }, { path: '/censorshipcheck', icon: '🚧', titleKey: 'censorshipcheck.Title', noteKey: 'advancedtools.CensorshipCheck', enabled: true }, diff --git a/frontend/components/ConnectivityTest.vue b/frontend/components/ConnectivityTest.vue index f798a11dc..995a67d1e 100644 --- a/frontend/components/ConnectivityTest.vue +++ b/frontend/components/ConnectivityTest.vue @@ -167,7 +167,7 @@ const connectivityTests = reactive([ // — including /favicon.ico — which the browser honors regardless of fetch // mode, then fails CORS on the woff2 files. speed.cloudflare.com serves a // plain favicon with no preload headers. - { id: 'cloudflare', name: 'Cloudflare', icon: 'ri:cloud-line', url: 'https://speed.cloudflare.com/favicon.ico', status: t('connectivity.StatusWait'), time: 0, mintime: 0, roundResults: [] }, + { id: 'cloudflare', name: 'Cloudflare', icon: 'simple-icons:cloudflare', url: 'https://speed.cloudflare.com/favicon.ico', status: t('connectivity.StatusWait'), time: 0, mintime: 0, roundResults: [] }, { id: 'youtube', name: 'YouTube', icon: 'ri:youtube-line', url: 'https://www.youtube.com/favicon.ico', status: t('connectivity.StatusWait'), time: 0, mintime: 0, roundResults: [] }, { id: 'github', name: 'GitHub', icon: 'ri:github-line', url: 'https://github.com/favicon.ico', status: t('connectivity.StatusWait'), time: 0, mintime: 0, roundResults: [] }, { id: 'chatgpt', name: 'ChatGPT', icon: 'ri:openai-line', url: 'https://chatgpt.com/favicon.ico', status: t('connectivity.StatusWait'), time: 0, mintime: 0, roundResults: [] }, diff --git a/frontend/components/ServiceStatus.vue b/frontend/components/ServiceStatus.vue new file mode 100644 index 000000000..8d6c373e0 --- /dev/null +++ b/frontend/components/ServiceStatus.vue @@ -0,0 +1,359 @@ + + + diff --git a/frontend/composables/use-refresh-orchestrator.js b/frontend/composables/use-refresh-orchestrator.js index f731192b7..57ca65d49 100644 --- a/frontend/composables/use-refresh-orchestrator.js +++ b/frontend/composables/use-refresh-orchestrator.js @@ -1,7 +1,7 @@ // Refresh / initial load sequence orchestration // // Input: -// - refs: { IPCheckRef, connectivityRef, webRTCRef, dnsLeaksRef } +// - refs: { IPCheckRef, connectivityRef, serviceStatusRef, webRTCRef, dnsLeaksRef } // - store: main store // - t: i18n translation function // - userPreferences: computed(() => store.userPreferences) @@ -40,10 +40,13 @@ export function useRefreshOrchestrator({ refs, store, t, userPreferences, infoMa store.setLoadingStatus('DNSLeakTest', false); store.setLoadingStatus('IPInfo', false); - const { IPCheckRef, connectivityRef, webRTCRef, dnsLeaksRef } = refs; + const { IPCheckRef, connectivityRef, serviceStatusRef, webRTCRef, dnsLeaksRef } = refs; scheduleTimedTasks([ { action: () => IPCheckRef.value.checkAllIPs(), delay: 0 }, { action: () => connectivityRef.value.handelCheckStart(true), delay: 2000 }, + // ServiceStatus is display-only (not in LOADING_SECTIONS): just re-pull, + // no loadingStatus reset needed. + { action: () => serviceStatusRef.value.refresh(), delay: 1500 }, { action: () => webRTCRef.value.checkAllWebRTC(true), delay: 3000 }, { action: () => dnsLeaksRef.value.checkAllDNSLeakTest(true), delay: 2500 }, { action: () => refreshingAlert(), delay: 500 }, diff --git a/frontend/data/changelog.json b/frontend/data/changelog.json index fefd6df34..9ce73120d 100644 --- a/frontend/data/changelog.json +++ b/frontend/data/changelog.json @@ -1225,5 +1225,20 @@ } } ] + }, + { + "version": "v6.5.0", + "date": "Beta", + "content": [ + { + "type": "add", + "change": { + "en": "Added a Service Status section showing live availability of popular services (Claude, OpenAI, GitHub, Cloudflare and more)", + "zh": "新增「服务可用性」板块,实时展示知名服务(Claude、OpenAI、GitHub、Cloudflare 等)的可用状态", + "fr": "Ajout d'une section État des services affichant la disponibilité en temps réel de services populaires (Claude, OpenAI, GitHub, Cloudflare, etc.)", + "tr": "Popüler servislerin (Claude, OpenAI, GitHub, Cloudflare ve daha fazlası) canlı kullanılabilirliğini gösteren Servis Durumu bölümü eklendi" + } + } + ] } ] \ No newline at end of file diff --git a/frontend/data/sections.js b/frontend/data/sections.js index 33eca3b4a..4e264e3b2 100644 --- a/frontend/data/sections.js +++ b/frontend/data/sections.js @@ -10,6 +10,7 @@ export const SECTION_IDS = [ 'IPInfo', 'Connectivity', + 'ServiceStatus', 'WebRTC', 'DNSLeakTest', 'SpeedTest', diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 12d5d39bf..840d5d51b 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -141,6 +141,7 @@ "Navigation": "Navigation", "IPInfo": "IP Infos", "Connectivity": "Connectivity", + "ServiceStatus": "Service Status", "WebRTC": "WebRTC Leak Test", "DNSLeakTest": "DNS Leak Test", "SpeedTest": "Speed Test", @@ -600,6 +601,45 @@ "nativenessTooltip": "Whether the ASN's registered country matches the IP's geolocation country" } }, + "serviceStatus": { + "Title": "Service Status", + "Note": "Live availability of popular services, pulled from their official status pages. Expand a card to see its sub-services and recent incidents.", + "Refresh": "Refresh Service Status", + "AllOperational": "All Systems Operational", + "ComponentsTitle": "Services", + "IncidentsTitle": "Recent Incidents", + "NoComponents": "No service details available.", + "NoRecentIncidents": "No recent incidents.", + "Loading": "Loading…", + "Checking": "Checking…", + "IncidentsError": "Couldn't load incidents.", + "ComponentsError": "Couldn't load services.", + "indicator": { + "minor": "Minor Issues", + "major": "Major Outage", + "critical": "Critical Outage", + "maintenance": "Under Maintenance", + "unknown": "Status unavailable" + }, + "component": { + "operational": "Operational", + "degraded_performance": "Degraded", + "partial_outage": "Partial Outage", + "major_outage": "Major Outage", + "under_maintenance": "Maintenance" + }, + "incidentStatus": { + "investigating": "Investigating", + "identified": "Identified", + "monitoring": "Monitoring", + "resolved": "Resolved", + "postmortem": "Postmortem", + "scheduled": "Scheduled", + "in_progress": "In Progress", + "verifying": "Verifying", + "completed": "Completed" + } + }, "connectivity": { "Title": "Network Connectivity", "Note": "Testing is done by loading small images from corresponding websites. Delay values are for reference only and will be smaller in reality. You can add other websites you frequently use to the test list for quick testing.", diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index 4308b8a6d..40ad17429 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -141,6 +141,7 @@ "Navigation": "Navigation", "IPInfo": "Infos IP", "Connectivity": "Connectivité", + "ServiceStatus": "État des services", "WebRTC": "Test WebRTC Leak", "DNSLeakTest": "Test de fuite DNS", "SpeedTest": "Test de vitesse", @@ -600,6 +601,45 @@ "nativenessTooltip": "Indique si le pays d'enregistrement de l'ASN correspond au pays de géolocalisation de l'IP" } }, + "serviceStatus": { + "Title": "État des services", + "Note": "Disponibilité en temps réel de services populaires, issue de leurs pages d'état officielles. Dépliez une carte pour voir ses sous-services et ses incidents récents.", + "Refresh": "Actualiser l'état des services", + "AllOperational": "Tous les systèmes sont opérationnels", + "ComponentsTitle": "Services", + "IncidentsTitle": "Incidents récents", + "NoComponents": "Aucun détail de service disponible.", + "NoRecentIncidents": "Aucun incident récent.", + "Loading": "Chargement…", + "Checking": "Vérification…", + "IncidentsError": "Impossible de charger les incidents.", + "ComponentsError": "Impossible de charger les services.", + "indicator": { + "minor": "Problèmes mineurs", + "major": "Panne majeure", + "critical": "Panne critique", + "maintenance": "En maintenance", + "unknown": "Statut indisponible" + }, + "component": { + "operational": "Opérationnel", + "degraded_performance": "Dégradé", + "partial_outage": "Panne partielle", + "major_outage": "Panne majeure", + "under_maintenance": "Maintenance" + }, + "incidentStatus": { + "investigating": "Enquête en cours", + "identified": "Identifié", + "monitoring": "Surveillance", + "resolved": "Résolu", + "postmortem": "Post-mortem", + "scheduled": "Planifié", + "in_progress": "En cours", + "verifying": "Vérification", + "completed": "Terminé" + } + }, "connectivity": { "Title": "Connectivité réseau", "Note": "Tests via chargement d'images à partir des sites correspondants. Les valeurs de délai sont données à titre indicatif seulement et seront plus petites en réalité. Vous pouvez ajouter d'autres sites web que vous utilisez fréquemment à la liste de tests pour des tests rapides.", diff --git a/frontend/locales/tr.json b/frontend/locales/tr.json index f443c97f2..2ba91b8db 100644 --- a/frontend/locales/tr.json +++ b/frontend/locales/tr.json @@ -141,6 +141,7 @@ "Navigation": "Navigasyon", "IPInfo": "IP Bilgileri", "Connectivity": "Bağlantı", + "ServiceStatus": "Servis Durumu", "WebRTC": "WebRTC Sızıntı Testi", "DNSLeakTest": "DNS Sızıntı Testi", "SpeedTest": "Hız Testi", @@ -600,6 +601,45 @@ "nativenessTooltip": "ASN'nin kayıtlı ülkesinin IP'nin coğrafi konum ülkesiyle eşleşip eşleşmediğini gösterir" } }, + "serviceStatus": { + "Title": "Servis Durumu", + "Note": "Popüler servislerin resmi durum sayfalarından alınan canlı kullanılabilirlik bilgisi. Alt servisleri ve son olayları görmek için bir kartı genişletin.", + "Refresh": "Servis Durumunu Yenile", + "AllOperational": "Tüm Sistemler Çalışıyor", + "ComponentsTitle": "Servisler", + "IncidentsTitle": "Son Olaylar", + "NoComponents": "Servis ayrıntısı yok.", + "NoRecentIncidents": "Yakın zamanda olay yok.", + "Loading": "Yükleniyor…", + "Checking": "Kontrol ediliyor…", + "IncidentsError": "Olaylar yüklenemedi.", + "ComponentsError": "Servisler yüklenemedi.", + "indicator": { + "minor": "Küçük Sorunlar", + "major": "Büyük Kesinti", + "critical": "Kritik Kesinti", + "maintenance": "Bakımda", + "unknown": "Durum alınamadı" + }, + "component": { + "operational": "Çalışıyor", + "degraded_performance": "Düşük Performans", + "partial_outage": "Kısmi Kesinti", + "major_outage": "Büyük Kesinti", + "under_maintenance": "Bakım" + }, + "incidentStatus": { + "investigating": "İnceleniyor", + "identified": "Tespit Edildi", + "monitoring": "İzleniyor", + "resolved": "Çözüldü", + "postmortem": "Değerlendirme", + "scheduled": "Planlandı", + "in_progress": "Sürüyor", + "verifying": "Doğrulanıyor", + "completed": "Tamamlandı" + } + }, "connectivity": { "Title": "Ağ Bağlantısı", "Note": "Test, ilgili web sitelerinden küçük resimler yüklenerek yapılır. Gecikme değerleri yalnızca referans amaçlıdır ve gerçekte daha küçük olacaktır. Sık kullandığınız diğer web sitelerini test listesine ekleyerek hızlı testler yapabilirsiniz.", diff --git a/frontend/locales/zh.json b/frontend/locales/zh.json index f930ba702..ff1345b9c 100644 --- a/frontend/locales/zh.json +++ b/frontend/locales/zh.json @@ -141,6 +141,7 @@ "Navigation": "导航", "IPInfo": "IP 信息", "Connectivity": "网络连通性", + "ServiceStatus": "服务可用性", "WebRTC": "WebRTC 泄露测试", "DNSLeakTest": "DNS 泄漏测试", "SpeedTest": "网速测试", @@ -600,6 +601,45 @@ "nativenessTooltip": "判断 ASN 的注册国家是否与 IP 的归属国家一致" } }, + "serviceStatus": { + "Title": "服务可用性", + "Note": "展示一些知名服务的实时可用状态,数据来自它们的官方状态页。展开卡片可查看各子服务的状态与最近的事故。", + "Refresh": "刷新服务状态", + "AllOperational": "所有系统正常", + "ComponentsTitle": "服务项", + "IncidentsTitle": "最近事故", + "NoComponents": "暂无服务详情。", + "NoRecentIncidents": "近期无事故。", + "Loading": "加载中…", + "Checking": "查询中…", + "IncidentsError": "无法加载事故信息。", + "ComponentsError": "无法加载服务项。", + "indicator": { + "minor": "部分异常", + "major": "重大故障", + "critical": "严重故障", + "maintenance": "维护中", + "unknown": "状态获取失败" + }, + "component": { + "operational": "正常", + "degraded_performance": "性能下降", + "partial_outage": "部分中断", + "major_outage": "重大中断", + "under_maintenance": "维护中" + }, + "incidentStatus": { + "investigating": "调查中", + "identified": "已定位", + "monitoring": "监控中", + "resolved": "已解决", + "postmortem": "复盘", + "scheduled": "已计划", + "in_progress": "进行中", + "verifying": "验证中", + "completed": "已完成" + } + }, "connectivity": { "Title": "网络连通性", "Note": "通过加载对应网站上的小图片进行测试,延迟值仅供参考,实际会更小。你可以添加其它你常用的网站到测试列表中,方便快速测试。", diff --git a/frontend/utils/service-status-tone.js b/frontend/utils/service-status-tone.js new file mode 100644 index 000000000..91069da59 --- /dev/null +++ b/frontend/utils/service-status-tone.js @@ -0,0 +1,89 @@ +// Pure mapping helpers from Statuspage/incident.io status vocab to the four +// business tones used across the app (see composables/use-status-tone.js): +// 'wait' | 'ok-fast' | 'ok-slow' | 'fail' +// +// Framework-agnostic (no vue import) so it lives in utils/ and is unit-tested +// in tests/service-status-transform.test.js. Components feed these tones into +// useStatusTone()'s dotClass/textClass — never hand-rolling color classes. + +// Per-provider overall `status.indicator` → tone. +// none → ok-fast (all green) +// minor / maintenance → ok-slow (amber) +// unknown (upstream down) → wait (sky, "can't tell") +// major / critical → fail (red) +export function indicatorToTone(indicator) { + switch (indicator) { + case 'none': + return 'ok-fast'; + case 'minor': + case 'maintenance': + return 'ok-slow'; + case 'major': + case 'critical': + return 'fail'; + default: + return 'wait'; + } +} + +// Per-component `status` → tone. +// operational → ok-fast +// degraded_performance / under_maintenance → ok-slow +// partial_outage → ok-slow (amber, partial) +// major_outage → fail +export function componentStatusToTone(status) { + switch (status) { + case 'operational': + return 'ok-fast'; + case 'degraded_performance': + case 'partial_outage': + case 'under_maintenance': + return 'ok-slow'; + case 'major_outage': + return 'fail'; + default: + return 'wait'; + } +} + +// Incident severity (`impact`) → number of "!" marks to render. Severity is +// shown by icon count, not color (the color carries the lifecycle status, see +// incidentStatusTone), so this returns a count rather than a tone. +// none → 0 · minor → 1 · major → 2 · critical → 3 +export function impactLevel(impact) { + switch (impact) { + case 'minor': + return 1; + case 'major': + return 2; + case 'critical': + return 3; + default: + return 0; + } +} + +// Incident lifecycle `status` → tone. Reflects where the incident is in its +// life, not how severe it was — so a resolved problem reads as "done" (green) +// regardless of past severity. +// resolved / postmortem / completed → ok-fast (green, done) +// investigating → fail (red, acute / cause unknown) +// identified / monitoring / in_progress / verifying → ok-slow (amber, being handled) +// scheduled / other → wait (muted, upcoming / unknown) +export function incidentStatusTone(status) { + switch (status) { + case 'resolved': + case 'postmortem': + case 'completed': + return 'ok-fast'; + case 'investigating': + return 'fail'; + case 'identified': + case 'monitoring': + case 'in_progress': + case 'verifying': + return 'ok-slow'; + default: + return 'wait'; + } +} diff --git a/tests/api-handlers.test.js b/tests/api-handlers.test.js index 194a59f1e..a4f6f0b34 100644 --- a/tests/api-handlers.test.js +++ b/tests/api-handlers.test.js @@ -22,6 +22,10 @@ import macCheckerHandler from '../api/mac-checker.js'; import updateAchievementHandler from '../api/update-user-achievement.js'; import ipcheckIngHandler from '../api/ipcheck-ing.js'; import { getSessionResult as dnsLeakGetResult } from '../api/dns-leak-test.js'; +import serviceStatusHandler, { + componentsHandler as serviceStatusComponentsHandler, + incidentsHandler as serviceStatusIncidentsHandler, +} from '../api/service-status.js'; // -- shared test utilities ------------------------------------------------ @@ -304,3 +308,49 @@ describe('ipcheck-ing handler', () => { assert.deepEqual(res.body, { error: 'API key is missing' }); }); }); + +// -- service-status handlers ---------------------------------------------- + +describe('service-status overview handler', () => { + it('serves the in-memory overview shape without any upstream call', async () => { + const res = createResponse(); + await serviceStatusHandler(createRequest(), res); + // Before the poller's first tick the snapshot is empty, but the shape + // ({ updatedAt, providers[] }) is always present — and nothing was fetched. + assert.equal(res.statusCode, 200); + assert.ok(Array.isArray(res.body.providers), 'providers must be an array'); + assert.ok('updatedAt' in res.body, 'overview must carry updatedAt'); + }); +}); + +describe('service-status components handler', () => { + it('rejects non-GET with 405', async () => { + const res = createResponse(); + await serviceStatusComponentsHandler(createRequest({ method: 'POST', query: { id: 'claude' } }), res); + assert.equal(res.statusCode, 405); + }); + + it('serves a components array (empty until the poller fills the snapshot)', async () => { + const res = createResponse(); + await serviceStatusComponentsHandler(createRequest({ query: { id: 'claude' } }), res); + assert.equal(res.statusCode, 200); + assert.equal(res.body.id, 'claude'); + assert.ok(Array.isArray(res.body.components)); + }); +}); + +describe('service-status incidents handler', () => { + it('rejects non-GET with 405', async () => { + const res = createResponse(); + await serviceStatusIncidentsHandler(createRequest({ method: 'POST', query: { id: 'claude' } }), res); + assert.equal(res.statusCode, 405); + }); + + it('serves an incidents array (empty until the poller fills the snapshot)', async () => { + const res = createResponse(); + await serviceStatusIncidentsHandler(createRequest({ query: { id: 'claude' } }), res); + assert.equal(res.statusCode, 200); + assert.equal(res.body.id, 'claude'); + assert.ok(Array.isArray(res.body.incidents)); + }); +}); diff --git a/tests/composable-refresh-orchestrator.test.js b/tests/composable-refresh-orchestrator.test.js index 89ac38c91..d3277ead5 100644 --- a/tests/composable-refresh-orchestrator.test.js +++ b/tests/composable-refresh-orchestrator.test.js @@ -27,12 +27,13 @@ function makeStoreStub({ mountedFlags = {}, shouldRefresh = false, autoStart = f } function makeRefs() { - const calls = { ip: 0, conn: [], web: [], dns: [] }; - const IPCheckRef = ref({ checkAllIPs: () => { calls.ip += 1; } }); - const connectivityRef = ref({ handelCheckStart: (flag) => { calls.conn.push(flag); } }); - const webRTCRef = ref({ checkAllWebRTC: (flag) => { calls.web.push(flag); } }); - const dnsLeaksRef = ref({ checkAllDNSLeakTest: (flag) => { calls.dns.push(flag); } }); - return { refs: { IPCheckRef, connectivityRef, webRTCRef, dnsLeaksRef }, calls }; + const calls = { ip: 0, conn: [], svc: 0, web: [], dns: [] }; + const IPCheckRef = ref({ checkAllIPs: () => { calls.ip += 1; } }); + const connectivityRef = ref({ handelCheckStart: (flag) => { calls.conn.push(flag); } }); + const serviceStatusRef = ref({ refresh: () => { calls.svc += 1; } }); + const webRTCRef = ref({ checkAllWebRTC: (flag) => { calls.web.push(flag); } }); + const dnsLeaksRef = ref({ checkAllDNSLeakTest: (flag) => { calls.dns.push(flag); } }); + return { refs: { IPCheckRef, connectivityRef, serviceStatusRef, webRTCRef, dnsLeaksRef }, calls }; } describe('useRefreshOrchestrator()', () => { @@ -102,6 +103,7 @@ describe('useRefreshOrchestrator()', () => { assert.equal(calls.ip, 1, 'ipcheck refreshes'); assert.deepEqual(calls.conn, [true], 'connectivity refresh with forced flag'); + assert.equal(calls.svc, 1, 'service status re-pulled on full refresh'); assert.deepEqual(calls.web, [true]); assert.deepEqual(calls.dns, [true]); assert.equal(infoMaskLevel.value, 0, 'info mask reset on refresh'); diff --git a/tests/guards.test.js b/tests/guards.test.js index a07461b28..ad87d6664 100644 --- a/tests/guards.test.js +++ b/tests/guards.test.js @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { requireReferer, requireValidIP, requireValidPrefix } from '../common/guards.js'; +import { requireReferer, requireValidIP, requireValidPrefix, requireValidProviderId } from '../common/guards.js'; // Minimal (req, res, next) stubs — just enough to observe what the // middleware does. @@ -129,3 +129,31 @@ describe('requireValidPrefix', () => { assert.equal(res.statusCode, 400); }); }); + +describe('requireValidProviderId', () => { + const guard = requireValidProviderId(); + + it('calls next() for a whitelisted provider id', () => { + let nextCalled = false; + guard(makeReq({ query: { id: 'claude' } }), makeRes(), () => { nextCalled = true; }); + assert.equal(nextCalled, true); + }); + + it('returns 400 "No provider id provided" when id is missing', () => { + const res = makeRes(); + let nextCalled = false; + guard(makeReq({ query: {} }), res, () => { nextCalled = true; }); + assert.equal(res.statusCode, 400); + assert.equal(res.body.error, 'No provider id provided'); + assert.equal(nextCalled, false); + }); + + it('returns 400 "Invalid provider id" for an unknown id', () => { + const res = makeRes(); + let nextCalled = false; + guard(makeReq({ query: { id: 'not-a-provider' } }), res, () => { nextCalled = true; }); + assert.equal(res.statusCode, 400); + assert.equal(res.body.error, 'Invalid provider id'); + assert.equal(nextCalled, false); + }); +}); diff --git a/tests/sections.test.js b/tests/sections.test.js index c40bb8fec..273556a73 100644 --- a/tests/sections.test.js +++ b/tests/sections.test.js @@ -8,10 +8,11 @@ import { } from '../frontend/data/sections.js'; describe('SECTION_IDS', () => { - it('lists all 6 section DOM ids in render order', () => { + it('lists all 7 section DOM ids in render order', () => { assert.deepEqual(SECTION_IDS, [ 'IPInfo', 'Connectivity', + 'ServiceStatus', 'WebRTC', 'DNSLeakTest', 'SpeedTest', @@ -22,11 +23,12 @@ describe('SECTION_IDS', () => { }); describe('createMountingStatus()', () => { - it('returns all 6 section ids as mount keys, set to false', () => { + it('returns all 7 section ids as mount keys, set to false', () => { const s = createMountingStatus(); assert.deepEqual(s, { IPInfo: false, Connectivity: false, + ServiceStatus: false, DNSLeakTest: false, WebRTC: false, SpeedTest: false, diff --git a/tests/service-status-transform.test.js b/tests/service-status-transform.test.js new file mode 100644 index 000000000..3564fada8 --- /dev/null +++ b/tests/service-status-transform.test.js @@ -0,0 +1,161 @@ +// Unit tests for the network-free Service Status normalizers and the +// status-vocab → tone mapping. No live upstream calls — inline fixtures only. + +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { normalizeSummary, normalizeIncidents, assembleProvider } from '../common/service-status-transform.js'; +import { + indicatorToTone, componentStatusToTone, impactLevel, incidentStatusTone, +} from '../frontend/utils/service-status-tone.js'; + +describe('normalizeSummary', () => { + it('extracts indicator and component statuses', () => { + const json = { + status: { indicator: 'none', description: 'All Systems Operational' }, + components: [ + { id: 'a', name: 'API', status: 'operational' }, + { id: 'b', name: 'Web', status: 'degraded_performance' }, + ], + }; + const out = normalizeSummary(json); + assert.equal(out.indicator, 'none'); + assert.equal(out.components.length, 2); + assert.deepEqual(out.components[0], { name: 'API', status: 'operational' }); + }); + + it('keeps only first-level rows: group headers stay, nested children drop', () => { + const json = { + status: { indicator: 'minor', description: 'Partial' }, + components: [ + { id: 'g1', name: 'Core', status: 'operational', group: true }, + { id: 'c1', name: 'Login', status: 'operational', group_id: 'g1' }, + { id: 'c2', name: 'Search', status: 'partial_outage', group_id: 'g1' }, + { id: 's1', name: 'Standalone', status: 'operational' }, + ], + }; + const out = normalizeSummary(json); + // Top-level header 'Core' + standalone 'Standalone'; children dropped. + assert.equal(out.components.length, 2); + assert.deepEqual(out.components.map((c) => c.name), ['Core', 'Standalone']); + }); + + it('caps the component list at the limit', () => { + const components = Array.from({ length: 50 }, (_, i) => ({ + id: `c${i}`, name: `svc${i}`, status: 'operational', + })); + const out = normalizeSummary({ status: {}, components }, 40); + assert.equal(out.components.length, 40); + }); + + it('defaults indicator to "unknown" on a malformed payload', () => { + const out = normalizeSummary({}); + assert.equal(out.indicator, 'unknown'); + assert.deepEqual(out.components, []); + }); +}); + +describe('normalizeIncidents', () => { + it('maps fields, builds the detail url from page+id, respects limit', () => { + const json = { + incidents: [ + { id: 'abc', name: 'Outage', status: 'resolved', impact: 'major', started_at: '2026-06-05T00:00:00Z' }, + { id: 'def', name: 'Slowness', status: 'monitoring', impact: 'minor', created_at: '2026-06-04T00:00:00Z' }, + { id: 'ghi', name: 'Old', status: 'resolved', impact: 'none' }, + ], + }; + const out = normalizeIncidents(json, { limit: 2, pageUrl: 'https://status.openai.com' }); + assert.equal(out.length, 2); + assert.deepEqual(out[0], { + id: 'abc', name: 'Outage', status: 'resolved', impact: 'major', + startedAt: '2026-06-05T00:00:00Z', + url: 'https://status.openai.com/incidents/abc', + }); + // Falls back to created_at when started_at is absent. + assert.equal(out[1].startedAt, '2026-06-04T00:00:00Z'); + }); + + it('trims a trailing slash on pageUrl before building the url', () => { + const out = normalizeIncidents( + { incidents: [{ id: 'x1', name: 'I' }] }, + { pageUrl: 'https://status.openai.com/' }, + ); + assert.equal(out[0].url, 'https://status.openai.com/incidents/x1'); + }); + + it('falls back to the page url when an incident has no id', () => { + const out = normalizeIncidents( + { incidents: [{ name: 'No id' }] }, + { pageUrl: 'https://status.example.com' }, + ); + assert.equal(out[0].url, 'https://status.example.com'); + }); + + it('returns an empty array on a malformed payload', () => { + assert.deepEqual(normalizeIncidents({}), []); + assert.deepEqual(normalizeIncidents(null), []); + }); +}); + +describe('assembleProvider', () => { + const provider = { id: 'openai', name: 'OpenAI', page: 'https://status.openai.com', api: 'https://status.openai.com' }; + + it('combines config + summary + incidents into the frontend shape', () => { + const summaryJson = { + status: { indicator: 'minor', description: 'Partial' }, + components: [{ id: 'a', name: 'API', status: 'degraded_performance' }], + }; + const incidentsJson = { incidents: [{ id: 'i1', name: 'Latency', status: 'monitoring', impact: 'minor' }] }; + const out = assembleProvider(provider, summaryJson, incidentsJson); + assert.equal(out.id, 'openai'); + assert.equal(out.name, 'OpenAI'); + assert.equal(out.page, 'https://status.openai.com'); + assert.equal(out.indicator, 'minor'); + assert.equal(out.components.length, 1); + assert.equal(out.incidents[0].url, 'https://status.openai.com/incidents/i1'); + }); + + it('degrades to "unknown" with empty lists when payloads are null (failed fetch)', () => { + const out = assembleProvider(provider, null, null); + assert.equal(out.indicator, 'unknown'); + assert.deepEqual(out.components, []); + assert.deepEqual(out.incidents, []); + }); +}); + +describe('tone mapping', () => { + it('maps provider indicators to tones', () => { + assert.equal(indicatorToTone('none'), 'ok-fast'); + assert.equal(indicatorToTone('minor'), 'ok-slow'); + assert.equal(indicatorToTone('maintenance'), 'ok-slow'); + assert.equal(indicatorToTone('major'), 'fail'); + assert.equal(indicatorToTone('critical'), 'fail'); + assert.equal(indicatorToTone('unknown'), 'wait'); + }); + + it('maps component statuses to tones', () => { + assert.equal(componentStatusToTone('operational'), 'ok-fast'); + assert.equal(componentStatusToTone('degraded_performance'), 'ok-slow'); + assert.equal(componentStatusToTone('partial_outage'), 'ok-slow'); + assert.equal(componentStatusToTone('major_outage'), 'fail'); + assert.equal(componentStatusToTone('under_maintenance'), 'ok-slow'); + }); + + it('maps incident impact to a "!" count (no color)', () => { + assert.equal(impactLevel('none'), 0); + assert.equal(impactLevel('minor'), 1); + assert.equal(impactLevel('major'), 2); + assert.equal(impactLevel('critical'), 3); + assert.equal(impactLevel(undefined), 0); + }); + + it('colors the incident status by lifecycle, not severity', () => { + assert.equal(incidentStatusTone('resolved'), 'ok-fast'); + assert.equal(incidentStatusTone('postmortem'), 'ok-fast'); + assert.equal(incidentStatusTone('completed'), 'ok-fast'); + assert.equal(incidentStatusTone('investigating'), 'fail'); + assert.equal(incidentStatusTone('identified'), 'ok-slow'); + assert.equal(incidentStatusTone('monitoring'), 'ok-slow'); + assert.equal(incidentStatusTone('scheduled'), 'wait'); + }); +}); From 0c99bf7c5c2ab7ade46265d4e7e32306e949d564 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sat, 6 Jun 2026 14:43:52 +0800 Subject: [PATCH 03/35] Improvements --- README.md | 3 +- README_FR.md | 3 +- README_TR.md | 3 +- README_ZH.md | 3 +- frontend/components/ServiceStatus.vue | 40 +++++++++++++-------------- frontend/locales/en.json | 4 +-- frontend/locales/fr.json | 4 +-- frontend/locales/tr.json | 4 +-- frontend/locales/zh.json | 4 +-- 9 files changed, 35 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index c6353d383..367d0df8a 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,13 @@ Feel free to bookmark the demo or deploy your own. * 🕵️ **IP Information**: Presents detailed information for all IP addresses, including country, region, ASN, geographic location, and more. * 🛰️ **ASN History & Upstream Topology**: View historical AS announcements for an IP prefix, and visualize the upstream paths from an ASN to the Tier 1 backbone networks. * 🚦 **Availability Check**: Tests the accessibility of various websites, such as Google, GitHub, YouTube, ChatGPT, and others. +* 📡 **Service Status**: Shows the live availability of well-known services (Claude, OpenAI, GitHub, Cloudflare, and more) from their official status pages, with per-service status and recent incidents. * 🚥 **WebRTC Detection**: Identifies the IP address used during WebRTC connections. * 🛑 **DNS Leak Test**: Shows DNS endpoint data to evaluate the risk of DNS leaks when using VPNs or proxies. * 🚀 **Speed Test**:Test your network speed with edge networks. * 🚏 **Proxy Rule Testing**: Test the rule settings of proxy software to ensure their correctness. * ⏱️ **Global Latency Test**: Performe lantency tests on servers located in different regions around the world. -* 📡 **MTR Test**: Perform MTR tests on servers located in different regions around the world. +* 🚉 **MTR Test**: Perform MTR tests on servers located in different regions around the world. * 🔦 **DNS Resolver**: Performs DNS resolution of a domain name from multiple sources and obtains real-time resolution results that can be used for contamination determination. * 🚧 **Censorship Check**: Check if a website is blocked in some countries. * 📓 **Whois Search**: Perform whois information search for domain names or IP addresses diff --git a/README_FR.md b/README_FR.md index 9bfa13df9..854c2dd3c 100644 --- a/README_FR.md +++ b/README_FR.md @@ -35,12 +35,13 @@ Notes: Vous pouvez utiliser ma démo gratuitement et vous pouvez également la d * 🕵️ **Informations sur l'adresse IP** : Présente des informations détaillées pour toutes les adresses IP, y compris le pays, la région, l'ASN, la localisation géographique, et plus encore. * 🛰️ **Historique ASN et topologie amont** : Consultez l'historique des annonces AS pour un préfixe IP, et visualisez les chemins amont d'un ASN vers les réseaux dorsaux Tier 1. * 🚦 **Vérification de disponibilité** : Teste l'accessibilité de différents sites web, tels que Google, GitHub, YouTube, ChatGPT, et d'autres. +* 📡 **État des services** : Affiche la disponibilité en temps réel de services populaires (Claude, OpenAI, GitHub, Cloudflare, etc.) depuis leurs pages d'état officielles, avec l'état de chaque sous-service et les incidents récents. * 🚥 **Détection WebRTC** : Identifie l'adresse IP utilisée lors des connexions WebRTC. * 🛑 **Test de fuite DNS** : Affiche les données de point de terminaison DNS pour évaluer le risque de fuites DNS lors de l'utilisation de VPN ou de proxies. * 🚀 **Test de vitesse** : Testez la vitesse de votre réseau avec des réseaux de pointe. * 🚏 **Test de règles** : Teste si les paramètres de règles fonctionnent correctement avec le logiciel de proxy. * ⏱️ **Test de latence mondiale** : Effectue des tests de latence sur des serveurs situés dans différentes régions du monde. -* 📡 **Test MTR** : Effectue des tests MTR sur des serveurs situés dans différentes régions du monde. +* 🚉 **Test MTR** : Effectue des tests MTR sur des serveurs situés dans différentes régions du monde. * 🔦 **Résolveur DNS** : effectue la résolution DNS d'un nom de domaine à partir de plusieurs sources, obtient les résultats de la résolution en temps réel et peut être utilisé pour la détermination de la contamination. * 🚧 **Test de Censorship**: Vérifier si un site est bloqué dans certains pays. * 📓 **Recherche Whois** : Effectuer une recherche d'informations Whois pour les noms de domaine ou les adresses IP diff --git a/README_TR.md b/README_TR.md index 1aabf18d9..041e2a841 100644 --- a/README_TR.md +++ b/README_TR.md @@ -35,12 +35,13 @@ Demo'yu yer imlerine ekleyebilir veya kendi kurulumunuzu yapabilirsiniz. * 🕵️ **IP Bilgileri**: Ülke, bölge, ASN, coğrafi konum ve daha fazlasını içeren ayrıntılı IP bilgileri sunar. * 🛰️ **ASN Geçmişi ve Üst Topoloji**: Bir IP önekinin geçmiş AS duyurularını ve ASN'den Tier 1 omurga ağlarına giden üst bağlantı yollarını görüntüleyin. * 🚦 **Erişilebilirlik Kontrolü**: Google, GitHub, YouTube, ChatGPT ve diğerleri gibi sitelerin erişilebilirliğini test eder. +* 📡 **Servis Durumu**: Tanınmış servislerin (Claude, OpenAI, GitHub, Cloudflare vb.) resmi durum sayfalarından alınan canlı kullanılabilirliğini, her alt servisin durumu ve son olaylarla birlikte gösterir. * 🚥 **WebRTC Tespiti**: WebRTC bağlantısında kullanılan IP adresini belirler. * 🛑 **DNS Leak Testi**: VPN veya proxy kullanırken DNS sızıntısı riskini değerlendirmek için DNS uç nokta verilerini gösterir. * 🚀 **Hız Testi**:Edge ağlarıyla ağ hızınızı test edin. * 🚏 **Proxy Kural Testi**: Proxy yazılımlarının kural ayarlarını doğru çalışıp çalışmadığını test edin. * ⏱️ **Küresel Gecikme Testi**: Dünyanın farklı bölgelerindeki sunuculara gecikme testleri yapın. -* 📡 **MTR Testi**: Dünya çapındaki sunucular için MTR testleri gerçekleştirin. +* 🚉 **MTR Testi**: Dünya çapındaki sunucular için MTR testleri gerçekleştirin. * 🔦 **DNS Çözücüsü**: Bir alan adının birden fazla kaynaktan DNS çözümlemesini yapar ve gerçek zamanlı çözümleme sonuçları alır. * 🚧 **Sansür Kontrolü**: Bir web sitesinin bazı ülkelerde engellenip engellenmediğini kontrol edin. * 📓 **Whois Arama**: Alan adı veya IP adresi için whois bilgisi sorgulayın. diff --git a/README_ZH.md b/README_ZH.md index 5a3905695..945f64cb9 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -35,12 +35,13 @@ * 🕵️ **看 IP 信息**:显示所有 IP 的相关信息,包括国家、地区、ASN、地理位置等 * 🛰️ **ASN 历史与上游拓扑**:查看 IP 前缀的历史 AS 宣告记录,以及该 ASN 到 Tier 1 骨干网的上游路径图 * 🚦 **可用性检测**:检测一些网站的可用性:Google, Github, Youtube, 网易, 百度等 +* 📡 **服务可用性**:展示一些知名服务(Claude、OpenAI、GitHub、Cloudflare 等)的实时可用状态,数据来自它们的官方状态页,可查看各子服务状态与最近的事故 * 🚥 **WebRTC 检测**:查看使用 WebRTC 连接时使用的 IP * 🛑 **DNS 泄露检测**:查看 DNS 出口信息,以便查看在 VPN/代理的情况下,是否存在 DNS 泄露隐私的风险 * 🚀 **网速测试**:利用边缘网络进行网速测试 * 🚏 **代理规则测试**:配合代理软件的规则设置,测试规则设置是否正常 * ⏱️ **全球延迟测试**:从分布在全球的多个服务器进行延迟测试,了解你与全球网络的连接速度 -* 📡 **MTR 测试**:从分布在全球的多个服务器进行 MTR 测试,了解你与全球的连接路径 +* 🚉 **MTR 测试**:从分布在全球的多个服务器进行 MTR 测试,了解你与全球的连接路径 * 🔦 **DNS 解析器**:从多个渠道对域名进行 DNS 解析,获取实时的解析结果,可用于污染判断 * 🚧 **封锁测试**:检查特定的网站在部分国家是否被封锁 * 📓 **Whois 查询**:对域名或 IP 进行 whois 信息查询 diff --git a/frontend/components/ServiceStatus.vue b/frontend/components/ServiceStatus.vue index 8d6c373e0..3b40e4762 100644 --- a/frontend/components/ServiceStatus.vue +++ b/frontend/components/ServiceStatus.vue @@ -20,11 +20,10 @@ -
+ fetch shows per-card "status unavailable" rather than removing cards. --> +
- + @@ -63,8 +62,7 @@ is fetched on first view, not with the overview. --> - + {{ t('serviceStatus.ComponentsTitle') }} {{ t('serviceStatus.IncidentsTitle') }} @@ -118,15 +116,15 @@
-
- {{ formatDate(inc.startedAt) }} - -
+
+ {{ formatDate(inc.startedAt) }} + +
{{ incidentStatusLabel(inc.status) }} @@ -185,14 +183,14 @@ const { textClass } = useStatusTone(); // reuses the generic cloud glyph; providers with no reliable `ri` brand glyph // are left null and fall back to a first-letter tile. const PROVIDERS = [ - { id: 'claude', name: 'Claude', icon: 'ri:anthropic-fill' }, - { id: 'openai', name: 'OpenAI', icon: 'ri:openai-fill' }, + { id: 'claude', name: 'Claude', icon: 'ri:claude-line' }, + { id: 'openai', name: 'OpenAI', icon: 'ri:openai-line' }, { id: 'cursor', name: 'Cursor', icon: 'simple-icons:cursor' }, - { id: 'github', name: 'GitHub', icon: 'ri:github-fill' }, - { id: 'discord', name: 'Discord', icon: 'ri:discord-fill' }, + { id: 'notion', name: 'Notion', icon: 'ri:notion-line' }, + { id: 'github', name: 'GitHub', icon: 'ri:github-line' }, { id: 'cloudflare', name: 'Cloudflare', icon: 'simple-icons:cloudflare' }, - { id: 'reddit', name: 'Reddit', icon: 'ri:reddit-fill' }, - { id: 'notion', name: 'Notion', icon: 'ri:notion-fill' }, + { id: 'discord', name: 'Discord', icon: 'ri:discord-line' }, + { id: 'reddit', name: 'Reddit', icon: 'ri:reddit-line' }, ]; const placeholderSizes = [10, 7, 9, 6, 8]; diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 840d5d51b..52529e43d 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -603,7 +603,7 @@ }, "serviceStatus": { "Title": "Service Status", - "Note": "Live availability of popular services, pulled from their official status pages. Expand a card to see its sub-services and recent incidents.", + "Note": "When a service won't open, it might not be your problem — the service itself could be down. This section shows the live availability of well-known services; expand a card to see each sub-service's status and recent incidents.", "Refresh": "Refresh Service Status", "AllOperational": "All Systems Operational", "ComponentsTitle": "Services", @@ -642,7 +642,7 @@ }, "connectivity": { "Title": "Network Connectivity", - "Note": "Testing is done by loading small images from corresponding websites. Delay values are for reference only and will be smaller in reality. You can add other websites you frequently use to the test list for quick testing.", + "Note": "This test checks whether you can reach a given website. It works by loading a small image from that site. Delay values are for reference only and will be smaller in reality. You can add other websites you frequently use to the test list for quick testing.", "StatusWait": "Awaiting Test", "StatusAvailable": "OK", "StatusUnavailable": "Unavailable", diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index 40ad17429..77b7e1eb3 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -603,7 +603,7 @@ }, "serviceStatus": { "Title": "État des services", - "Note": "Disponibilité en temps réel de services populaires, issue de leurs pages d'état officielles. Dépliez une carte pour voir ses sous-services et ses incidents récents.", + "Note": "Quand un service ne s'ouvre pas, le problème ne vient pas forcément de vous — le service lui-même peut être en panne. Cette section affiche la disponibilité en temps réel de services populaires ; dépliez une carte pour voir l'état de chaque sous-service et les incidents récents.", "Refresh": "Actualiser l'état des services", "AllOperational": "Tous les systèmes sont opérationnels", "ComponentsTitle": "Services", @@ -642,7 +642,7 @@ }, "connectivity": { "Title": "Connectivité réseau", - "Note": "Tests via chargement d'images à partir des sites correspondants. Les valeurs de délai sont données à titre indicatif seulement et seront plus petites en réalité. Vous pouvez ajouter d'autres sites web que vous utilisez fréquemment à la liste de tests pour des tests rapides.", + "Note": "Ce test vérifie si vous pouvez atteindre un site web donné. Il fonctionne en chargeant une petite image depuis ce site. Les valeurs de délai sont données à titre indicatif seulement et seront plus petites en réalité. Vous pouvez ajouter d'autres sites web que vous utilisez fréquemment à la liste de tests pour des tests rapides.", "StatusWait": "En attente du test", "StatusAvailable": "OK", "StatusUnavailable": "Non disponible", diff --git a/frontend/locales/tr.json b/frontend/locales/tr.json index 2ba91b8db..b99c8f051 100644 --- a/frontend/locales/tr.json +++ b/frontend/locales/tr.json @@ -603,7 +603,7 @@ }, "serviceStatus": { "Title": "Servis Durumu", - "Note": "Popüler servislerin resmi durum sayfalarından alınan canlı kullanılabilirlik bilgisi. Alt servisleri ve son olayları görmek için bir kartı genişletin.", + "Note": "Bir servis açılmadığında sorun sizde olmayabilir — servisin kendisi çökmüş olabilir. Bu bölüm tanınmış servislerin canlı kullanılabilirliğini gösterir; her alt servisin durumunu ve son olayları görmek için bir kartı genişletin.", "Refresh": "Servis Durumunu Yenile", "AllOperational": "Tüm Sistemler Çalışıyor", "ComponentsTitle": "Servisler", @@ -642,7 +642,7 @@ }, "connectivity": { "Title": "Ağ Bağlantısı", - "Note": "Test, ilgili web sitelerinden küçük resimler yüklenerek yapılır. Gecikme değerleri yalnızca referans amaçlıdır ve gerçekte daha küçük olacaktır. Sık kullandığınız diğer web sitelerini test listesine ekleyerek hızlı testler yapabilirsiniz.", + "Note": "Bu test, belirli bir web sitesine ulaşıp ulaşamadığınızı kontrol eder. İlgili siteden küçük bir resim yüklenerek çalışır. Gecikme değerleri yalnızca referans amaçlıdır ve gerçekte daha küçük olacaktır. Sık kullandığınız diğer web sitelerini test listesine ekleyerek hızlı testler yapabilirsiniz.", "StatusWait": "Test Bekleniyor", "StatusAvailable": "Tamam", "StatusUnavailable": "Kullanılamıyor", diff --git a/frontend/locales/zh.json b/frontend/locales/zh.json index ff1345b9c..2f65f9b27 100644 --- a/frontend/locales/zh.json +++ b/frontend/locales/zh.json @@ -603,7 +603,7 @@ }, "serviceStatus": { "Title": "服务可用性", - "Note": "展示一些知名服务的实时可用状态,数据来自它们的官方状态页。展开卡片可查看各子服务的状态与最近的事故。", + "Note": "当你打开某些服务的时候,可能并不是你的问题,而是这些服务出故障了。本板块展示一些知名服务的实时可用状态,展开卡片可查看各子服务的状态与最近的事故。", "Refresh": "刷新服务状态", "AllOperational": "所有系统正常", "ComponentsTitle": "服务项", @@ -642,7 +642,7 @@ }, "connectivity": { "Title": "网络连通性", - "Note": "通过加载对应网站上的小图片进行测试,延迟值仅供参考,实际会更小。你可以添加其它你常用的网站到测试列表中,方便快速测试。", + "Note": "本测试用于测试你是否能够连通到某个网站。测试方法是通过加载对应网站上的小图片进行的。延迟值仅供参考,实际会更小。你可以添加其它你常用的网站到测试列表中,方便快速测试。", "StatusWait": "待检测", "StatusAvailable": "可用", "StatusUnavailable": "不可用", From 5de1835108752f22f0b48811ae597e51511aafac Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sat, 6 Jun 2026 17:13:04 +0800 Subject: [PATCH 04/35] Improvements --- frontend/components/ConnectivityTest.vue | 13 ++++--- frontend/components/ServiceStatus.vue | 49 +++++++++++++----------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/frontend/components/ConnectivityTest.vue b/frontend/components/ConnectivityTest.vue index 995a67d1e..78bd42ebe 100644 --- a/frontend/components/ConnectivityTest.vue +++ b/frontend/components/ConnectivityTest.vue @@ -30,7 +30,9 @@ -
+
@@ -40,9 +42,7 @@
- + @@ -61,7 +61,8 @@
- @@ -253,7 +254,7 @@ const roundTooltipText = (test, idx) => { if (!entry) return ''; const n = idx + 1; if (entry.tone === 'fail') return t('connectivity.RoundCount', { n }) + t('connectivity.StatusUnavailable'); - return t('connectivity.RoundCount', { n}) + entry.time + ' ms'; + return t('connectivity.RoundCount', { n }) + entry.time + ' ms'; }; // Reachable: Smile <200ms, Meh ≥200ms. Unreachable: Frown. Wait: no face. diff --git a/frontend/components/ServiceStatus.vue b/frontend/components/ServiceStatus.vue index 3b40e4762..7c4217ecc 100644 --- a/frontend/components/ServiceStatus.vue +++ b/frontend/components/ServiceStatus.vue @@ -24,9 +24,10 @@
- - - + + +
- -
- - -
-
-
+ + +
+ + +
+ - + {{ t('serviceStatus.ComponentsTitle') }} {{ t('serviceStatus.IncidentsTitle') }} @@ -72,17 +72,17 @@ -
- + - @@ -50,18 +58,24 @@ - - {{ cards[openedCard].icon }}{{ t(cards[openedCard].titleKey) }} + {{ activeTool.emoji }}{{ t(activeTool.titleKey) }} - + + + +
- +
@@ -70,15 +84,15 @@ diff --git a/frontend/components/Home.vue b/frontend/components/Home.vue new file mode 100644 index 000000000..7b8338ea5 --- /dev/null +++ b/frontend/components/Home.vue @@ -0,0 +1,122 @@ + + + diff --git a/frontend/components/StandaloneTool.vue b/frontend/components/StandaloneTool.vue new file mode 100644 index 000000000..ea94ed4a0 --- /dev/null +++ b/frontend/components/StandaloneTool.vue @@ -0,0 +1,64 @@ + + + diff --git a/frontend/components/advanced-tools/GlobalLatencyTest.vue b/frontend/components/advanced-tools/GlobalLatencyTest.vue index bcebf9873..70c79bb30 100644 --- a/frontend/components/advanced-tools/GlobalLatencyTest.vue +++ b/frontend/components/advanced-tools/GlobalLatencyTest.vue @@ -6,11 +6,14 @@

{{ t('pingtest.Note2') }}

- +
- +
- @@ -18,7 +21,12 @@ {{ ip }} -