diff --git a/.npmrc b/.npmrc index 0c05da4..b6f27f1 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1 @@ engine-strict=true -resolution-mode=highest diff --git a/Caddyfile b/Caddyfile index 0dae9f3..821c804 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,5 +1,10 @@ :{$PORT:80} root * /app encode gzip zstd -try_files {path}.html {path} + +# SPA fallback for /admin path +handle /admin* { + try_files {path} /admin/index.html +} + file_server diff --git a/package-lock.json b/package-lock.json index 9d7e9e4..e8cd104 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "ipaddr.js": "^2.2.0", "js-xxhash": "^4.0.0", "json5": "^2.2.3", + "svelte-i18n": "^4.0.1", "svelte-jsoneditor": "^2.4.0", "unplugin-icons": "^22.1.0" }, @@ -612,9 +613,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -641,13 +642,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -656,9 +657,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -679,10 +680,23 @@ "node": "*" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -693,9 +707,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", - "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -705,7 +719,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, @@ -717,9 +731,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -741,19 +755,22 @@ } }, "node_modules/@eslint/js": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", - "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -761,13 +778,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.12.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -799,6 +816,57 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz", + "integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.1", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz", + "integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/icu-skeleton-parser": "1.8.14", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.14", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz", + "integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz", + "integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "6.7.2", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", @@ -1447,9 +1515,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.6.tgz", - "integrity": "sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", + "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", "cpu": [ "arm" ], @@ -1461,9 +1529,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.6.tgz", - "integrity": "sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", + "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", "cpu": [ "arm64" ], @@ -1475,9 +1543,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.6.tgz", - "integrity": "sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", + "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", "cpu": [ "arm64" ], @@ -1489,9 +1557,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.6.tgz", - "integrity": "sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", + "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", "cpu": [ "x64" ], @@ -1503,9 +1571,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.6.tgz", - "integrity": "sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", + "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", "cpu": [ "arm64" ], @@ -1517,9 +1585,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.6.tgz", - "integrity": "sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", + "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", "cpu": [ "x64" ], @@ -1531,9 +1599,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.6.tgz", - "integrity": "sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", + "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", "cpu": [ "arm" ], @@ -1545,9 +1613,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.6.tgz", - "integrity": "sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", + "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", "cpu": [ "arm" ], @@ -1559,9 +1627,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.6.tgz", - "integrity": "sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", + "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", "cpu": [ "arm64" ], @@ -1573,9 +1641,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.6.tgz", - "integrity": "sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz", + "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", "cpu": [ "arm64" ], @@ -1586,10 +1654,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.6.tgz", - "integrity": "sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", + "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", "cpu": [ "loong64" ], @@ -1600,10 +1668,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.6.tgz", - "integrity": "sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", + "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", "cpu": [ "ppc64" ], @@ -1615,9 +1683,23 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.6.tgz", - "integrity": "sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", + "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", + "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", "cpu": [ "riscv64" ], @@ -1629,9 +1711,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.6.tgz", - "integrity": "sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", + "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", "cpu": [ "s390x" ], @@ -1643,9 +1725,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.6.tgz", - "integrity": "sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", + "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", "cpu": [ "x64" ], @@ -1657,9 +1739,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.6.tgz", - "integrity": "sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", + "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", "cpu": [ "x64" ], @@ -1670,10 +1752,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", + "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.6.tgz", - "integrity": "sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", + "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", "cpu": [ "arm64" ], @@ -1685,9 +1781,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.6.tgz", - "integrity": "sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", + "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", "cpu": [ "ia32" ], @@ -1698,10 +1794,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz", + "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.6.tgz", - "integrity": "sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz", + "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", "cpu": [ "x64" ], @@ -1741,6 +1851,23 @@ "integrity": "sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==", "license": "ISC" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz", + "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, "node_modules/@sveltejs/adapter-auto": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-4.0.0.tgz", @@ -1765,17 +1892,19 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.17.3", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.17.3.tgz", - "integrity": "sha512-GcNaPDr0ti4O/TonPewkML2DG7UVXkSxPN3nPMlpmx0Rs4b2kVP4gymz98WEHlfzPXdd4uOOT1Js26DtieTNBQ==", + "version": "2.49.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.2.tgz", + "integrity": "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==", "dev": true, "license": "MIT", "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", "cookie": "^0.6.0", - "devalue": "^5.1.0", + "devalue": "^5.3.2", "esm-env": "^1.2.2", - "import-meta-resolve": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", @@ -1790,9 +1919,15 @@ "node": ">=18.13" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.3 || ^6.0.0" + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } } }, "node_modules/@sveltejs/kit/node_modules/esm-env": { @@ -1879,9 +2014,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/json-schema": { @@ -2229,9 +2364,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2522,9 +2657,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", - "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", "dev": true, "funding": [ { @@ -2601,6 +2736,22 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2722,6 +2873,19 @@ "node": ">=4" } }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -2739,6 +2903,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -2760,7 +2930,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2780,9 +2949,9 @@ } }, "node_modules/devalue": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", - "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.1.tgz", + "integrity": "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==", "dev": true, "license": "MIT" }, @@ -2888,6 +3057,58 @@ "dev": true, "license": "MIT" }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, "node_modules/esbuild": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", @@ -2953,32 +3174,32 @@ } }, "node_modules/eslint": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", - "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", - "@eslint/core": "^0.12.0", - "@eslint/eslintrc": "^3.3.0", - "@eslint/js": "9.21.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -3076,9 +3297,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3106,9 +3327,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -3117,9 +3338,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3149,16 +3370,31 @@ "dev": true, "license": "MIT" }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3168,9 +3404,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3245,6 +3481,16 @@ "node": ">=0.10.0" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/expect-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", @@ -3255,6 +3501,15 @@ "node": ">=12.0.0" } }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3452,9 +3707,9 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -3498,6 +3753,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "license": "MIT" + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "license": "MIT" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -3597,6 +3864,18 @@ "node": ">=0.8.19" } }, + "node_modules/intl-messageformat": { + "version": "10.7.16", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz", + "integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.2", + "tslib": "^2.8.0" + } + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -3678,6 +3957,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", @@ -3739,9 +4024,9 @@ } }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -3800,9 +4085,9 @@ } }, "node_modules/jsonpath-plus": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.2.0.tgz", - "integrity": "sha512-T9V+8iNYKFL2n2rF+w02LBOT2JjDnTjioaNFrxRy0Bv1y/hNsqR/EBK7Ojy2ythRHwmz2cRIls+9JitQGZC/sw==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", + "integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==", "license": "MIT", "dependencies": { "@jsep-plugin/assignment": "^1.3.0", @@ -3969,6 +4254,15 @@ "dev": true, "license": "ISC" }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -3991,6 +4285,25 @@ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", "license": "MIT" }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4073,7 +4386,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -4139,6 +4451,12 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "license": "MIT" }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -4816,13 +5134,13 @@ } }, "node_modules/rollup": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.6.tgz", - "integrity": "sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz", + "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -4832,25 +5150,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.6", - "@rollup/rollup-android-arm64": "4.34.6", - "@rollup/rollup-darwin-arm64": "4.34.6", - "@rollup/rollup-darwin-x64": "4.34.6", - "@rollup/rollup-freebsd-arm64": "4.34.6", - "@rollup/rollup-freebsd-x64": "4.34.6", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.6", - "@rollup/rollup-linux-arm-musleabihf": "4.34.6", - "@rollup/rollup-linux-arm64-gnu": "4.34.6", - "@rollup/rollup-linux-arm64-musl": "4.34.6", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.6", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.6", - "@rollup/rollup-linux-riscv64-gnu": "4.34.6", - "@rollup/rollup-linux-s390x-gnu": "4.34.6", - "@rollup/rollup-linux-x64-gnu": "4.34.6", - "@rollup/rollup-linux-x64-musl": "4.34.6", - "@rollup/rollup-win32-arm64-msvc": "4.34.6", - "@rollup/rollup-win32-ia32-msvc": "4.34.6", - "@rollup/rollup-win32-x64-msvc": "4.34.6", + "@rollup/rollup-android-arm-eabi": "4.53.5", + "@rollup/rollup-android-arm64": "4.53.5", + "@rollup/rollup-darwin-arm64": "4.53.5", + "@rollup/rollup-darwin-x64": "4.53.5", + "@rollup/rollup-freebsd-arm64": "4.53.5", + "@rollup/rollup-freebsd-x64": "4.53.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", + "@rollup/rollup-linux-arm-musleabihf": "4.53.5", + "@rollup/rollup-linux-arm64-gnu": "4.53.5", + "@rollup/rollup-linux-arm64-musl": "4.53.5", + "@rollup/rollup-linux-loong64-gnu": "4.53.5", + "@rollup/rollup-linux-ppc64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-musl": "4.53.5", + "@rollup/rollup-linux-s390x-gnu": "4.53.5", + "@rollup/rollup-linux-x64-gnu": "4.53.5", + "@rollup/rollup-linux-x64-musl": "4.53.5", + "@rollup/rollup-openharmony-arm64": "4.53.5", + "@rollup/rollup-win32-arm64-msvc": "4.53.5", + "@rollup/rollup-win32-ia32-msvc": "4.53.5", + "@rollup/rollup-win32-x64-gnu": "4.53.5", + "@rollup/rollup-win32-x64-msvc": "4.53.5", "fsevents": "~2.3.2" } }, @@ -4882,7 +5203,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, "license": "MIT", "dependencies": { "mri": "^1.1.0" @@ -5257,21 +5577,6 @@ } } }, - "node_modules/svelte-check/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/svelte-eslint-parser": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.0.0.tgz", @@ -5338,58 +5643,494 @@ "@floating-ui/dom": "^1.5.3" } }, - "node_modules/svelte-jsoneditor": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/svelte-jsoneditor/-/svelte-jsoneditor-2.4.0.tgz", - "integrity": "sha512-AqAXVJ3mH8UidVj0ZCM4VSmwSZtapmlu1hF7qyGlotsC1zyCIZ6V+ZkSIUxLUGxXB8v9AGIOxLSrgpPnS1yg1Q==", - "license": "ISC", + "node_modules/svelte-i18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/svelte-i18n/-/svelte-i18n-4.0.1.tgz", + "integrity": "sha512-jaykGlGT5PUaaq04JWbJREvivlCnALtT+m87Kbm0fxyYHynkQaxQMnIKHLm2WeIuBRoljzwgyvz0Z6/CMwfdmQ==", + "license": "MIT", "dependencies": { - "@codemirror/autocomplete": "^6.18.1", - "@codemirror/commands": "^6.7.1", - "@codemirror/lang-json": "^6.0.1", - "@codemirror/language": "^6.10.3", - "@codemirror/lint": "^6.8.2", - "@codemirror/search": "^6.5.6", - "@codemirror/state": "^6.4.1", - "@codemirror/view": "^6.34.1", - "@fortawesome/free-regular-svg-icons": "^6.6.0", - "@fortawesome/free-solid-svg-icons": "^6.6.0", - "@jsonquerylang/jsonquery": "^3.1.1 || ^4.0.0", - "@lezer/highlight": "^1.2.1", - "@replit/codemirror-indentation-markers": "^6.5.3", - "ajv": "^8.17.1", - "codemirror-wrapped-line-indent": "^1.0.8", - "diff-sequences": "^29.6.3", - "immutable-json-patch": "^6.0.1", - "jmespath": "^0.16.0", - "json-source-map": "^0.6.1", - "jsonpath-plus": "^9.0.0 || ^10.2.0", - "jsonrepair": "^3.0.0", - "lodash-es": "^4.17.21", - "memoize-one": "^6.0.0", - "natural-compare-lite": "^1.4.0", - "sass": "^1.80.4", - "svelte-awesome": "^3.3.5", - "svelte-select": "^5.8.3", - "vanilla-picker": "^2.12.3" + "cli-color": "^2.0.3", + "deepmerge": "^4.2.2", + "esbuild": "^0.19.2", + "estree-walker": "^2", + "intl-messageformat": "^10.5.3", + "sade": "^1.8.1", + "tiny-glob": "^0.2.9" + }, + "bin": { + "svelte-i18n": "dist/cli.js" + }, + "engines": { + "node": ">= 16" }, "peerDependencies": { - "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0" + "svelte": "^3 || ^4 || ^5" } }, - "node_modules/svelte-jsoneditor/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "node_modules/svelte-i18n/node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/svelte-i18n/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/svelte-jsoneditor": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/svelte-jsoneditor/-/svelte-jsoneditor-2.4.0.tgz", + "integrity": "sha512-AqAXVJ3mH8UidVj0ZCM4VSmwSZtapmlu1hF7qyGlotsC1zyCIZ6V+ZkSIUxLUGxXB8v9AGIOxLSrgpPnS1yg1Q==", + "license": "ISC", + "dependencies": { + "@codemirror/autocomplete": "^6.18.1", + "@codemirror/commands": "^6.7.1", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/language": "^6.10.3", + "@codemirror/lint": "^6.8.2", + "@codemirror/search": "^6.5.6", + "@codemirror/state": "^6.4.1", + "@codemirror/view": "^6.34.1", + "@fortawesome/free-regular-svg-icons": "^6.6.0", + "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@jsonquerylang/jsonquery": "^3.1.1 || ^4.0.0", + "@lezer/highlight": "^1.2.1", + "@replit/codemirror-indentation-markers": "^6.5.3", + "ajv": "^8.17.1", + "codemirror-wrapped-line-indent": "^1.0.8", + "diff-sequences": "^29.6.3", + "immutable-json-patch": "^6.0.1", + "jmespath": "^0.16.0", + "json-source-map": "^0.6.1", + "jsonpath-plus": "^9.0.0 || ^10.2.0", + "jsonrepair": "^3.0.0", + "lodash-es": "^4.17.21", + "memoize-one": "^6.0.0", + "natural-compare-lite": "^1.4.0", + "sass": "^1.80.4", + "svelte-awesome": "^3.3.5", + "svelte-select": "^5.8.3", + "vanilla-picker": "^2.12.3" + }, + "peerDependencies": { + "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/svelte-jsoneditor/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, @@ -5589,6 +6330,29 @@ "node": ">=0.8" } }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "license": "MIT", + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -5602,6 +6366,54 @@ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinypool": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", @@ -5681,6 +6493,12 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5836,15 +6654,18 @@ } }, "node_modules/vite": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", - "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -5968,6 +6789,37 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vitefu": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.5.tgz", diff --git a/package.json b/package.json index 6540639..dba9ae4 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "ipaddr.js": "^2.2.0", "js-xxhash": "^4.0.0", "json5": "^2.2.3", + "svelte-i18n": "^4.0.1", "svelte-jsoneditor": "^2.4.0", "unplugin-icons": "^22.1.0" } diff --git a/src/app.html b/src/app.html index 7226faa..6fb14df 100644 --- a/src/app.html +++ b/src/app.html @@ -1,14 +1,47 @@ - + - + + + %sveltekit.head%
%sveltekit.body% diff --git a/src/app.postcss b/src/app.postcss index d6bb6e4..d905e72 100644 --- a/src/app.postcss +++ b/src/app.postcss @@ -11,7 +11,7 @@ body { */ html, body { - @apply overflow-y-scroll; + @apply h-full overflow-hidden; } /* modern theme */ diff --git a/src/lib/Navigation.svelte b/src/lib/Navigation.svelte index 772d1de..71f80aa 100644 --- a/src/lib/Navigation.svelte +++ b/src/lib/Navigation.svelte @@ -9,11 +9,13 @@ import RawMdiRouter from '~icons/mdi/router'; import RawMdiSecurity from '~icons/mdi/security'; import RawMdiSettings from '~icons/mdi/settings'; + import RawMdiChartLine from '~icons/mdi/chart-line'; // import { ApiKeyInfoStore, ApiKeyStore, hasValidApi } from './Stores'; import { onMount, type Component } from 'svelte'; import { page } from '$app/state'; import { App } from '$lib/States.svelte'; + import { _ } from 'svelte-i18n'; type NavigationProps = { labels?: boolean @@ -26,7 +28,7 @@ const DrawerStore = getDrawerStore(); function classesActive(href: string): string { - return href === page.route.id ? 'bg-primary-300 dark:bg-primary-700' : ''; + return href === page.route.id ? 'variant-filled-primary' : ''; } let newPath = $state(''); @@ -37,18 +39,18 @@ type Page = { path: string; - name: string; + nameKey: string; logo: Component; }; const allPages: Page[] = [ - { path: '/', name: 'Home', logo: RawMdiHome }, - { path: '/users', name: 'Users', logo: RawMdiPerson }, - { path: '/nodes', name: 'Nodes', logo: RawMdiDevices }, - { path: '/deploy', name: 'Deploy', logo: RawMdiHomeGroupPlus }, - { path: '/routes', name: 'Routes', logo: RawMdiRouter }, - { path: '/acls', name: 'ACLs', logo: RawMdiSecurity }, - { path: '/settings', name: 'Settings', logo: RawMdiSettings }, + { path: '/', nameKey: 'navigation.home', logo: RawMdiHome }, + { path: '/users', nameKey: 'navigation.users', logo: RawMdiPerson }, + { path: '/nodes', nameKey: 'navigation.nodes', logo: RawMdiDevices }, + { path: '/deploy', nameKey: 'navigation.deploy', logo: RawMdiHomeGroupPlus }, + { path: '/routes', nameKey: 'navigation.routes', logo: RawMdiRouter }, + { path: '/acls', nameKey: 'navigation.acls', logo: RawMdiSecurity }, + { path: '/settings', nameKey: 'navigation.settings', logo: RawMdiSettings }, ].filter((p) => p != undefined); const pages = $derived.by(() => App.hasValidApi ? allPages : allPages.slice(-1)); @@ -70,7 +72,7 @@ {#if labels} - {p.name} + {$_(p.nameKey)} {/if} diff --git a/src/lib/States.svelte.ts b/src/lib/States.svelte.ts index 022847e..1a9f05e 100644 --- a/src/lib/States.svelte.ts +++ b/src/lib/States.svelte.ts @@ -7,6 +7,8 @@ import type { ToastStore } from '@skeletonlabs/skeleton'; import { apiGet } from './common/api'; import { arraysEqual, clone, toastError, toastWarning } from './common/funcs'; import { debug } from './common/debug'; +import { _ } from 'svelte-i18n'; +import { get } from 'svelte/store'; export type LayoutStyle = 'tile' | 'list'; @@ -40,12 +42,12 @@ export class State { } -// state that is wrapped in LocalStorage +// state that is wrapped in LocalStorage or SessionStorage export class StateLocal { #key: string; #value = $state() as T; #effect?: (value?: T) => void; - // #saver = $derived(this.save(this.#value)) + #session: boolean; get key() { return this.#key; @@ -57,31 +59,34 @@ export class StateLocal { set value(value: T) { this.#value = value; - // this.save(this.#value); if(this.#effect !== undefined) { this.#effect(value); } } + get storage(): Storage | null { + if (!browser) return null; + return this.#session ? sessionStorage : localStorage; + } + save(value: T) { - debug(`Saving '${this.#key}' in localStorage...`); - localStorage.setItem(this.#key, this.serialize(value)); + debug(`Saving '${this.#key}' in ${this.#session ? 'sessionStorage' : 'localStorage'}...`); + this.storage?.setItem(this.#key, this.serialize(value)); } - constructor(key: string, valueDefault: T, effect?: (value?: T) => void) { + constructor(key: string, valueDefault: T, effect?: (value?: T) => void, session: boolean = false) { this.#key = key; this.#effect = effect; + this.#value = valueDefault; + this.#session = session; if(browser){ - const storedValue = localStorage.getItem(this.#key); + const storedValue = this.storage?.getItem(this.#key); if (storedValue) { this.#value = this.deserialize(storedValue); - } else { - this.#value = valueDefault; } - // how do I clean this up? $effect.root(()=>{ $effect(()=>{ this.save(this.#value); @@ -116,10 +121,30 @@ export class HeadscaleAdmin { } }) + // language settings + language = new StateLocal('locale', 'en') + // api info apiValid = $state(false); apiUrl = new StateLocal('apiUrl', ''); - apiKey = new StateLocal('apiKey', ''); + + #apiKey = new StateLocal('apiKey', ''); + #apiKeySession = new StateLocal('apiKey', '', undefined, true); + + get apiKey(): StateLocal { + const key = this.apiRememberMe.value ? this.#apiKey : this.#apiKeySession; + // If we switched from remember to not remember, or vice versa, ensure the key is transferred + if (this.apiRememberMe.value && !this.#apiKey.value && this.#apiKeySession.value) { + this.#apiKey.value = this.#apiKeySession.value; + this.#apiKeySession.value = ''; + } else if (!this.apiRememberMe.value && !this.#apiKeySession.value && this.#apiKey.value) { + this.#apiKeySession.value = this.#apiKey.value; + this.#apiKey.value = ''; + } + return key; + } + + apiRememberMe = new StateLocal('apiRememberMe', true); apiTtl = new StateLocal('apiTTL', 10000); apiKeyInfo = new StateLocal('apiKeyInfo', { authorized: null, @@ -127,8 +152,8 @@ export class HeadscaleAdmin { informedUnauthorized: false, informedExpiringSoon: false, }) - hasApiKey = $derived(isInitialized() && !!this.apiKey.value) - hasApiUrl = $derived(isInitialized() && !!this.apiUrl.value) + hasApiKey = $derived(isInitialized() && !!this.apiKey.value) + hasApiUrl = $derived(isInitialized() && !!this.apiUrl.value) hasApi = $derived(this.hasApiKey && this.hasApiUrl) hasValidApi = $derived(this.hasApi && this.apiKeyInfo.value.authorized === true) @@ -212,6 +237,28 @@ export class HeadscaleAdmin { if (preAuthKeys === undefined) { preAuthKeys = await getPreAuthKeys() } + + // Keep full keys (no asterisks) for a given ID if the new one is masked + // This preserves the initially shown full key when API refreshes show masked version + preAuthKeys = preAuthKeys.map((newKey) => { + const existingKey = this.preAuthKeys.value.find(k => k.id === newKey.id); + + // Safety checks for key field + if (existingKey && + newKey.key && existingKey.key && + typeof newKey.key === 'string' && + typeof existingKey.key === 'string') { + + // If new key is masked (has asterisks) and existing is not, keep the full key + if (newKey.key.includes('*') && !existingKey.key.includes('*')) { + debug('Preserving full key for ID', newKey.id, '- old:', existingKey.key.substring(0, 20), 'new:', newKey.key.substring(0, 20)); + return existingKey; + } + } + + return newKey; + }); + if(!arraysEqual(this.preAuthKeys.value, preAuthKeys)){ this.preAuthKeys.value = [...preAuthKeys] return true @@ -221,10 +268,17 @@ export class HeadscaleAdmin { async populateApiKeyInfo(): Promise { const { apiKeys } = await apiGet(`/api/v1/apikey`); - const myKey = apiKeys.filter((key) => this.apiKey.value.startsWith(key.prefix))[0]; + const myKey = apiKeys.find((key) => { + const cleanPrefix = key.prefix.replace(/\*+$/, ''); + return this.apiKey.value.startsWith(cleanPrefix); + }); + const apiKeyInfo = this.apiKeyInfo.value - apiKeyInfo.expires = myKey.expiration; apiKeyInfo.authorized = true; + if (myKey) { + apiKeyInfo.expires = myKey.expiration; + } + this.apiKeyInfo.value = {...apiKeyInfo}; return true; } @@ -267,7 +321,7 @@ export class HeadscaleAdmin { } } -export const App = $state(new HeadscaleAdmin()) +export const App = new HeadscaleAdmin() function isInitialized(): boolean { @@ -293,7 +347,7 @@ export function informUserUnauthorized(toastStore: ToastStore) { } App.apiKeyInfo.value.informedUnauthorized = true; App.apiKeyInfo.value.authorized = false; - toastError('API Key is Unauthorized or Invalid', toastStore); + toastError(get(_)( 'settings.unauthorizedMessage' ), toastStore); }); } @@ -304,6 +358,6 @@ export function informUserExpiringSoon(toastStore: ToastStore) { } App.apiKeyInfo.value.informedUnauthorized = true; App.apiKeyInfo.value.authorized = false; - toastWarning('API Key Expires Soon', toastStore); + toastWarning(get(_)( 'settings.expiringSoonMessage' ), toastStore); }); } \ No newline at end of file diff --git a/src/lib/cards/CardListContainer.svelte b/src/lib/cards/CardListContainer.svelte index 2d87118..a3d8c84 100644 --- a/src/lib/cards/CardListContainer.svelte +++ b/src/lib/cards/CardListContainer.svelte @@ -2,6 +2,6 @@ let { children } = $props() -
+
{@render children()}
diff --git a/src/lib/cards/CardListItem.svelte b/src/lib/cards/CardListItem.svelte index 042104e..6924cae 100644 --- a/src/lib/cards/CardListItem.svelte +++ b/src/lib/cards/CardListItem.svelte @@ -13,7 +13,7 @@ diff --git a/src/lib/cards/CardListPage.svelte b/src/lib/cards/CardListPage.svelte index ac93317..06dc6ca 100644 --- a/src/lib/cards/CardListPage.svelte +++ b/src/lib/cards/CardListPage.svelte @@ -7,7 +7,7 @@ let { children }: CardListPageProps = $props() -
+
{@render children()} diff --git a/src/lib/cards/CardTileContainer.svelte b/src/lib/cards/CardTileContainer.svelte index fbcd783..28dfdc8 100644 --- a/src/lib/cards/CardTileContainer.svelte +++ b/src/lib/cards/CardTileContainer.svelte @@ -4,24 +4,36 @@ type CardTileContainerProps = { classes?: string, - onclick?: MouseEventHandler, + onclick?: MouseEventHandler, + href?: string, children: Snippet, } let { classes = '', onclick = undefined, + href = undefined, children, }: CardTileContainerProps = $props() const classesFinal = $derived( - 'col-span-12 xs:col-span-12 sm:col-span-6 md:col-span-6 lg:col-span-6 xl:col-span-4 2xl:col-span-3 card ' + - (onclick === undefined ? '' : 'card-hover ') + + 'col-span-12 xs:col-span-12 sm:col-span-6 md:col-span-6 lg:col-span-6 xl:col-span-4 2xl:col-span-3 card transition-all duration-200 ' + + (onclick === undefined && href === undefined ? '' : 'card-hover cursor-pointer ') + classes ); -
- +
+ {#if href} + + {@render children()} + + {:else if onclick} + + {:else} +
+ {@render children()} +
+ {/if}
\ No newline at end of file diff --git a/src/lib/cards/CardTilePage.svelte b/src/lib/cards/CardTilePage.svelte index 1055892..62750fa 100644 --- a/src/lib/cards/CardTilePage.svelte +++ b/src/lib/cards/CardTilePage.svelte @@ -6,7 +6,7 @@ } let { children }: CardTitlePageProps = $props() -
+
{@render children()}
diff --git a/src/lib/cards/acl/GroupListCard.svelte b/src/lib/cards/acl/GroupListCard.svelte index c449b04..496ce70 100644 --- a/src/lib/cards/acl/GroupListCard.svelte +++ b/src/lib/cards/acl/GroupListCard.svelte @@ -6,11 +6,12 @@ import Delete from '$lib/parts/Delete.svelte'; import MultiSelect from '$lib/parts/MultiSelect.svelte'; import Text from '$lib/parts/Text.svelte'; + import { _ } from 'svelte-i18n'; import { App } from '$lib/States.svelte'; import { debug } from '$lib/common/debug'; import { getToastStore } from '@skeletonlabs/skeleton'; - import { toastSuccess, toastError } from '$lib/common/funcs'; + import { toastSuccess, toastError, getUserAclName } from '$lib/common/funcs'; import RawMdiGroup from '~icons/mdi/account-group-outline'; @@ -25,9 +26,7 @@ let {acl = $bindable(), groupName, open = $bindable()}: GroupListCardProps = $props() const groupMembers = $derived(acl.getGroupMembers(groupName)); - const userNames = $derived(App.users.value.map((u) => { - return u.email ? u.email : u.name; - }).toSorted()) + const userNames = $derived(App.users.value.map(getUserAclName).toSorted()) let group = $state(makeGroup()); let groupNameNew = $state(''); @@ -47,7 +46,7 @@ try { if (groupName !== groupNameNew) { acl.renameGroup(groupName, groupNameNew); - toastSuccess(`Group renamed from '${groupName}' to '${groupNameNew}'`, ToastStore); + toastSuccess($_('cards.groupRenamedSuccess', { values: { oldName: groupName, newName: groupNameNew } }), ToastStore); groupName = groupNameNew; } return true; @@ -63,7 +62,7 @@ deleting = true; try { acl.deleteGroup(groupName); - toastSuccess(`Group '${groupName}' deleted`, ToastStore); + toastSuccess($_('cards.groupDeleted', { values: { name: groupName } }), ToastStore); } catch (e) { if (e instanceof Error) { toastError('', ToastStore, e); @@ -97,7 +96,7 @@ {#snippet children()}

- Members of + {$_('cards.membersOf')}
diff --git a/src/lib/cards/acl/HostListCard.svelte b/src/lib/cards/acl/HostListCard.svelte index 71b1e5c..17875e8 100644 --- a/src/lib/cards/acl/HostListCard.svelte +++ b/src/lib/cards/acl/HostListCard.svelte @@ -6,6 +6,7 @@ import Delete from '$lib/parts/Delete.svelte'; import { debug } from '$lib/common/debug'; import Text from '$lib/parts/Text.svelte'; + import { _ } from 'svelte-i18n'; const ToastStore = getToastStore(); @@ -74,7 +75,7 @@ function deleteHost() { try { acl.deleteHost(host.name); - toastSuccess(`Host '${host.name}' deleted`, ToastStore); + toastSuccess($_('cards.hostDeleted', { values: { name: host.name } }), ToastStore); } catch (e) { debug(e); if (e instanceof Error) { @@ -108,12 +109,12 @@ class="card p-4 variant-filled-warning text-center {popupShow ? '' : 'invisible'}" data-popup="popupHover-host-{hostName}" > -

Host '{hostName}' has the same name as a user.
Please rename the host.

+

{$_('cards.hostRenameWarning', { values: { hostName } })}

{#if userNames.includes(hostName)} diff --git a/src/lib/cards/acl/ListEntry.svelte b/src/lib/cards/acl/ListEntry.svelte index 45762ef..a5f9813 100644 --- a/src/lib/cards/acl/ListEntry.svelte +++ b/src/lib/cards/acl/ListEntry.svelte @@ -27,7 +27,7 @@ diff --git a/src/lib/cards/acl/PolicyListCard.svelte b/src/lib/cards/acl/PolicyListCard.svelte index 82d313b..b59526a 100644 --- a/src/lib/cards/acl/PolicyListCard.svelte +++ b/src/lib/cards/acl/PolicyListCard.svelte @@ -1,10 +1,11 @@ - + {#snippet children()}

- Sources: + {$_('cards.sources')}

{#each rule.src as src, i}
{src} @@ -204,7 +218,7 @@ {/each}

- Destinations: + {$_('cards.destinations')}

{#each rule.dst as dst, i}
{dst} @@ -269,7 +283,7 @@
{/each}

- Usernames: + {$_('cards.usernames')}

{ - const us = App.users.value.map(u => u.name) + const us = App.users.value.map(getUserAclName) us.sort() const gs = acl.getGroupNames(true) @@ -57,7 +58,7 @@ try { if (tag.name !== tagNameNew) { acl.renameTag(tag.name, tagNameNew); - toastSuccess(`Tag renamed from '${tag.name}' to '${tagNameNew}'`, ToastStore); + toastSuccess($_('cards.tagRenamedSuccess', { values: { oldName: tag.name, newName: tagNameNew } }), ToastStore); tagName = tagNameNew; } return true; @@ -76,7 +77,7 @@ loading = true try{ acl.deleteTag(tag.name); - toastSuccess(`Tag '${tag.name}' deleted`, ToastStore) + toastSuccess($_('cards.tagDeleted', { values: { name: tag.name } }), ToastStore) }catch(e){ if(e instanceof Error){ toastError('', ToastStore, e); @@ -101,7 +102,7 @@ {#snippet children()}

- Owners of + {$_('cards.ownersOf')}
diff --git a/src/lib/cards/common/ItemCreatedAt.svelte b/src/lib/cards/common/ItemCreatedAt.svelte index 3a0b36a..9c5a837 100644 --- a/src/lib/cards/common/ItemCreatedAt.svelte +++ b/src/lib/cards/common/ItemCreatedAt.svelte @@ -1,6 +1,8 @@ - - {new Date(item.createdAt).toLocaleString('en-Gb', { - minute: '2-digit', - year: 'numeric', - month: 'short', - day: '2-digit', - hour: '2-digit', - hour12: false, - })} + + {dateToStr(item.createdAt)} diff --git a/src/lib/cards/common/ItemDelete.svelte b/src/lib/cards/common/ItemDelete.svelte index d04f1f0..9ddb826 100644 --- a/src/lib/cards/common/ItemDelete.svelte +++ b/src/lib/cards/common/ItemDelete.svelte @@ -9,6 +9,7 @@ import { toastError, toastSuccess } from '$lib/common/funcs'; import Delete from '$lib/parts/Delete.svelte'; import { App } from '$lib/States.svelte'; + import { _ } from 'svelte-i18n'; type ItemDeleteProps = { item: Named, @@ -33,27 +34,27 @@ if (isUser(item)) { if (await deleteUser(item)) { - toastSuccess(`Deleted User "${name}" (ID: ${id})`, ToastStore); + toastSuccess($_('cards.deletedUser', { values: { name, id } }), ToastStore); DrawerStore.close() } else { - let msg = `Failed to Delete User "${name}" (${id}).`; + let msg = $_('cards.failedDeleteUser', { values: { name, id } }); if(App.nodes.value.some((node) => node.user.id === item.id)){ - msg += " Still has nodes." + msg += $_('cards.stillHasNodes'); } toastError(msg, ToastStore); } } if (isNode(item)) { if (await deleteNode(item)) { - toastSuccess(`Deleted machine "${name}" (${id})`, ToastStore); + toastSuccess($_('cards.deletedMachine', { values: { name, id } }), ToastStore); DrawerStore.close() } else { - toastError(`Failed to Delete Nachine "${name}" (${id})`, ToastStore); + toastError($_('cards.failedDeleteMachine', { values: { name, id } }), ToastStore); } } } - + diff --git a/src/lib/cards/common/ItemListName.svelte b/src/lib/cards/common/ItemListName.svelte index cd4b51e..0a0f561 100644 --- a/src/lib/cards/common/ItemListName.svelte +++ b/src/lib/cards/common/ItemListName.svelte @@ -6,6 +6,7 @@ import RawMdiRename from '~icons/mdi/rename'; import RawMdiCheckCircleOutline from '~icons/mdi/check-circle-outline'; import RawMdiCloseCircleOutline from '~icons/mdi/close-circle-outline'; + import { _ } from 'svelte-i18n'; import { renameNode, renameUser } from '$lib/common/api'; import { toastError, focus } from '$lib/common/funcs'; import { getToastStore } from '@skeletonlabs/skeleton'; @@ -32,7 +33,7 @@ const ToastStore = getToastStore(); - +
{#if showRename}
- {#each App.users.value as user} - + {/each} + + {:else} + + {diff.message} + + + + { + loading = true + try{ + App.updateValue(App.nodes, await expireNode(node)) + } finally { + loading = false + } + }} + /> + + {/if}
- + \ No newline at end of file diff --git a/src/lib/cards/node/NodeHostname.svelte b/src/lib/cards/node/NodeHostname.svelte index 81d03f2..fb0de63 100644 --- a/src/lib/cards/node/NodeHostname.svelte +++ b/src/lib/cards/node/NodeHostname.svelte @@ -1,6 +1,7 @@ - + {node.name} diff --git a/src/lib/cards/node/NodeInfo.svelte b/src/lib/cards/node/NodeInfo.svelte index b3924bb..8798c0d 100644 --- a/src/lib/cards/node/NodeInfo.svelte +++ b/src/lib/cards/node/NodeInfo.svelte @@ -38,8 +38,10 @@ - - + {#if node.availableRoutes.length > 0} + + + {/if} diff --git a/src/lib/cards/node/NodeLastSeen.svelte b/src/lib/cards/node/NodeLastSeen.svelte index 8bc9e85..0b45f4f 100644 --- a/src/lib/cards/node/NodeLastSeen.svelte +++ b/src/lib/cards/node/NodeLastSeen.svelte @@ -3,6 +3,7 @@ import type { Node } from '$lib/common/types'; import { onMount } from 'svelte'; import CardListEntry from '../CardListEntry.svelte'; + import { _ } from 'svelte-i18n'; type NodeLastSeenProps = { node: Node, @@ -21,9 +22,9 @@ }); - + {#if node.online} - Online Now + {$_('cards.onlineNow')} {:else} {lastSeen} {/if} diff --git a/src/lib/cards/node/NodeListCard.svelte b/src/lib/cards/node/NodeListCard.svelte index 45e505c..8206d77 100644 --- a/src/lib/cards/node/NodeListCard.svelte +++ b/src/lib/cards/node/NodeListCard.svelte @@ -6,29 +6,52 @@ import CardListEntry from '../CardListEntry.svelte'; import NodeInfo from './NodeInfo.svelte'; import OnlineNodeIndicator from '$lib/parts/OnlineNodeIndicator.svelte'; + import { _ } from 'svelte-i18n'; type NodeListCardProps = { node: Node, open?: boolean, + selectable?: boolean, + selected?: boolean, + onToggleSelect?: (nodeId: string) => void, } - let { node = $bindable(), open = $bindable(false) }: NodeListCardProps = $props() + let { + node = $bindable(), + open = $bindable(false), + selectable = false, + selected = false, + onToggleSelect = () => {}, + }: NodeListCardProps = $props() + + function handleCheckboxClick(event: Event) { + event.stopPropagation(); + onToggleSelect(node.id); + } + {#if selectable} + + {/if}
- + {node.givenName}
diff --git a/src/lib/cards/node/NodeOwner.svelte b/src/lib/cards/node/NodeOwner.svelte index 3c7625b..90e66fb 100644 --- a/src/lib/cards/node/NodeOwner.svelte +++ b/src/lib/cards/node/NodeOwner.svelte @@ -2,17 +2,9 @@ import CardListEntry from '../CardListEntry.svelte'; import type { Node } from '$lib/common/types'; import OnlineUserIndicator from '$lib/parts/OnlineUserIndicator.svelte'; - import { changeNodeOwner } from '$lib/common/api'; - import { openDrawer, toastError, toastSuccess } from '$lib/common/funcs'; - import { debug } from '$lib/common/debug'; - import { getDrawerStore, getToastStore } from '@skeletonlabs/skeleton'; - import { slide } from 'svelte/transition'; - - import RawMdiSwapHorizontal from '~icons/mdi/swap-horizontal'; - import RawMdiCheckCircleOutline from '~icons/mdi/check-circle-outline'; - import RawMdiCloseCircleOutline from '~icons/mdi/close-circle-outline'; - - import { App } from '$lib/States.svelte'; + import { openDrawer } from '$lib/common/funcs'; + import { getDrawerStore } from '@skeletonlabs/skeleton'; + import { _ } from 'svelte-i18n'; type NodeOwnerProps = { node: Node, @@ -20,17 +12,11 @@ let { node }: NodeOwnerProps = $props() const drawerStore = getDrawerStore(); - let transferUser = $state(''); - let showTransfer = $state(false); - let transferring = $state(false); - - const ToastStore = getToastStore(); - +
- { @@ -39,65 +25,6 @@ > {node.user.name} - -
- {#if showTransfer} -
- New Owner: - - - -
- {/if}
diff --git a/src/lib/cards/node/NodeRegistrationMethod.svelte b/src/lib/cards/node/NodeRegistrationMethod.svelte index fad99ed..14b67c9 100644 --- a/src/lib/cards/node/NodeRegistrationMethod.svelte +++ b/src/lib/cards/node/NodeRegistrationMethod.svelte @@ -1,6 +1,7 @@ - + {nodeRegMethod} diff --git a/src/lib/cards/node/NodeRoutes.svelte b/src/lib/cards/node/NodeRoutes.svelte index 860462c..47e6372 100644 --- a/src/lib/cards/node/NodeRoutes.svelte +++ b/src/lib/cards/node/NodeRoutes.svelte @@ -3,6 +3,7 @@ import type { Node } from '$lib/common/types'; import { debug } from '$lib/common/debug' import NodeRoute from './NodeRoute.svelte'; + import { _ } from 'svelte-i18n'; import ToggleOff from '~icons/mdi/toggle-switch-variant-off'; import ToggleOn from '~icons/mdi/toggle-switch-variant'; import { disableRoutes, enableRoutes } from '$lib/common/api'; @@ -25,41 +26,45 @@ - -
- - -
+ + {#if node.availableRoutes.length > 0} +
+ + +
+ {/if} {#if childBottom === undefined} {#each node.availableRoutes as route}
diff --git a/src/lib/cards/node/NodeTags.svelte b/src/lib/cards/node/NodeTags.svelte index 534f41d..609f274 100644 --- a/src/lib/cards/node/NodeTags.svelte +++ b/src/lib/cards/node/NodeTags.svelte @@ -1,12 +1,12 @@ -
-

The following tags have been prevented by the current ACL:

-

- {#if popupInvalidTagsShow == true} - {#each tagsInvalid as tag} - - {/each} - {/if} -

-
-
-
- + - - {#snippet childTitle()} - - Advertised Tags: - {#if tagsInvalid.length > 0} - - {/if} - - {/snippet} -
- {#each tagsValid as tag} - - {/each} -
-
diff --git a/src/lib/cards/node/NodeTileCard.svelte b/src/lib/cards/node/NodeTileCard.svelte index 5214bff..ffc02d3 100644 --- a/src/lib/cards/node/NodeTileCard.svelte +++ b/src/lib/cards/node/NodeTileCard.svelte @@ -14,16 +14,30 @@ import OnlineNodeIndicator from '$lib/parts/OnlineNodeIndicator.svelte'; import OnlineUserIndicator from '$lib/parts/OnlineUserIndicator.svelte'; import { App } from '$lib/States.svelte'; + import { _ } from 'svelte-i18n'; type NodeTileCardProps = { node: Node, + selectable?: boolean, + selected?: boolean, + onToggleSelect?: (nodeId: string) => void, } - let { node = $bindable() }: NodeTileCardProps = $props() + let { + node = $bindable(), + selectable = false, + selected = false, + onToggleSelect = () => {}, + }: NodeTileCardProps = $props() let lastSeen = $state(getTimeDifferenceMessage(getTime(node.lastSeen))); const routeCount = $derived(node.availableRoutes.length); const drawerStore = getDrawerStore(); + + function handleCheckboxClick(event: Event) { + event.stopPropagation(); + onToggleSelect(node.id); + } let color = $derived( (xxHash32(node.id + ':' + node.givenName, 0xbeefbabe) & 0xff_ff_ff) @@ -42,39 +56,60 @@ }); - openDrawer(drawerStore, 'nodeDrawer-' + node.id, node)}> + openDrawer(drawerStore, 'nodeDrawer-' + node.id, node)} class={selected ? 'ring-2 ring-primary-500' : ''}> + {#if selectable} +
+ +
+ {/if}
- ID: {node.id} + {$_('common.id')}: {node.id}
{node.givenName}
- + {dateToStr(node.createdAt)} - + {#if node.online} - Online Now + {$_('cards.onlineNow')} {:else} {lastSeen} {/if} - +
{node.user.name}
- +
{node.ipAddresses.filter((s) => /^\d+\.\d+\.\d+\.\d+$/.test(s)).at(0)}
- + {routeCount} + +
+ {#if node.tags.length > 0} + {#each node.tags as tag} + {tag.replace('tag:', '')} + {/each} + {:else} + - + {/if} +
+

diff --git a/src/lib/cards/route/RouteListCard.svelte b/src/lib/cards/route/RouteListCard.svelte index 538e767..309ada7 100644 --- a/src/lib/cards/route/RouteListCard.svelte +++ b/src/lib/cards/route/RouteListCard.svelte @@ -6,6 +6,7 @@ import CardListEntry from '../CardListEntry.svelte'; import RouteInfo from './RouteInfo.svelte'; import OnlineNodeIndicator from '$lib/parts/OnlineNodeIndicator.svelte'; + import { _ } from 'svelte-i18n'; type RouteListCardProps = { node: Node, @@ -18,7 +19,7 @@ @@ -27,7 +28,7 @@
- + {node.givenName}
diff --git a/src/lib/cards/route/RouteTileCard.svelte b/src/lib/cards/route/RouteTileCard.svelte index ed8807a..9764db0 100644 --- a/src/lib/cards/route/RouteTileCard.svelte +++ b/src/lib/cards/route/RouteTileCard.svelte @@ -8,6 +8,7 @@ import CardTileContainer from '../CardTileContainer.svelte'; import CardTileEntry from '../CardTileEntry.svelte'; import OnlineNodeIndicator from '$lib/parts/OnlineNodeIndicator.svelte'; + import { _ } from 'svelte-i18n'; import RouteInfo from './RouteInfo.svelte'; type RouteTileCardProps = { @@ -33,7 +34,7 @@
- ID: {node.id} + {$_('common.id')}: {node.id}
{node.givenName} diff --git a/src/lib/cards/user/UserCreate.svelte b/src/lib/cards/user/UserCreate.svelte index bba8675..17553ea 100644 --- a/src/lib/cards/user/UserCreate.svelte +++ b/src/lib/cards/user/UserCreate.svelte @@ -4,6 +4,7 @@ import { App } from '$lib/States.svelte'; import { getToastStore } from '@skeletonlabs/skeleton'; import RawMdiCheckCircleOutline from '~icons/mdi/check-circle-outline'; + import { _ } from 'svelte-i18n'; type UserCreateProps = { show: boolean, @@ -22,12 +23,12 @@ try { const u = await createUser(username); App.users.value.push(u) - toastSuccess('Created user "' + username + '"', toastStore); + toastSuccess($_('common.createdUser') + ' "' + username + '"', toastStore); show = false; username = ''; } catch (error) { if (error instanceof Error) { - toastError('Failed to create user "' + username + '"', toastStore, error); + toastError($_('common.failedCreateUser') + ' "' + username + '"', toastStore, error); } } finally { loading = false; @@ -38,9 +39,11 @@
import type { User } from '$lib/common/types'; import CardListEntry from '../CardListEntry.svelte'; + import { _ } from 'svelte-i18n'; type ItemCreatedAtProps = { user: User, @@ -10,6 +11,6 @@ - + {user.displayName} \ No newline at end of file diff --git a/src/lib/cards/user/UserEmail.svelte b/src/lib/cards/user/UserEmail.svelte index 98833e5..0e7bf65 100644 --- a/src/lib/cards/user/UserEmail.svelte +++ b/src/lib/cards/user/UserEmail.svelte @@ -1,6 +1,7 @@ - + {user.email} \ No newline at end of file diff --git a/src/lib/cards/user/UserListCard.svelte b/src/lib/cards/user/UserListCard.svelte index cfb78ca..a15e8a8 100644 --- a/src/lib/cards/user/UserListCard.svelte +++ b/src/lib/cards/user/UserListCard.svelte @@ -6,6 +6,7 @@ import CardListEntry from '../CardListEntry.svelte'; import UserInfo from './UserInfo.svelte'; import OnlineUserIndicator from '$lib/parts/OnlineUserIndicator.svelte'; + import { _ } from 'svelte-i18n'; type UserListCardProps = { user: User, @@ -17,7 +18,7 @@ @@ -26,7 +27,7 @@
- + {getUserDisplay(user)} diff --git a/src/lib/cards/user/UserListNodes.svelte b/src/lib/cards/user/UserListNodes.svelte index 7a180a5..4665a8d 100644 --- a/src/lib/cards/user/UserListNodes.svelte +++ b/src/lib/cards/user/UserListNodes.svelte @@ -5,6 +5,7 @@ import { openDrawer } from '$lib/common/funcs'; import { getDrawerStore } from '@skeletonlabs/skeleton'; import { App } from '$lib/States.svelte'; + import { _ } from 'svelte-i18n'; type UserListNodesProps = { user: User, @@ -12,17 +13,12 @@ } let { user = $bindable(), - title = 'Nodes:', + title = $_('cards.nodes'), }: UserListNodesProps = $props(); const drawerStore = getDrawerStore(); - const filteredNodes = $derived.by(() => { - if (App.users.value.filter((u) => u.id == user.id).length == 1) { - return App.nodes.value.filter((n) => n.user.id == user.id); - } - return []; - }); + const filteredNodes = $derived(App.nodes.value.filter((n) => n.user.id == user.id)); diff --git a/src/lib/cards/user/UserListPreAuthKey.svelte b/src/lib/cards/user/UserListPreAuthKey.svelte index ba15c40..41ac6a5 100644 --- a/src/lib/cards/user/UserListPreAuthKey.svelte +++ b/src/lib/cards/user/UserListPreAuthKey.svelte @@ -7,6 +7,7 @@ import { expirePreAuthKey, getPreAuthKeys } from '$lib/common/api'; import { App } from '$lib/States.svelte'; import { onMount } from 'svelte'; + import { _ } from 'svelte-i18n'; type UserListPreAuthKeyProps = { preAuthKey: PreAuthKey, @@ -33,26 +34,31 @@
- - - { - await expirePreAuthKey(preAuthKey); - const keys = await getPreAuthKeys([preAuthKey.user.id]); - keys.forEach((pak) => { - App.updateValue(App.preAuthKeys, pak) - }); - }} - /> - + {#if preAuthKey.key && preAuthKey.key.length > 0} + + {:else} +
+ ⚠️ + No key available +
+ {/if}
@@ -61,14 +67,14 @@ ? 'variant-ghost-success' : 'variant-flat opacity-50'}" > - Used + {$_('cards.used')} - Expired + {$_('cards.expired')}
@@ -77,15 +83,28 @@ ? 'variant-ghost-secondary' : 'variant-flat opacity-50'}" > - Ephemeral + {$_('cards.ephemeral')} - Reusable + {$_('cards.reusable')}
+
+ + { + await expirePreAuthKey(preAuthKey); + const keys = await getPreAuthKeys([preAuthKey.user.id]); + keys.forEach((pak) => { + App.updateValue(App.preAuthKeys, pak) + }); + }} + /> + +
diff --git a/src/lib/cards/user/UserListPreAuthKeys.svelte b/src/lib/cards/user/UserListPreAuthKeys.svelte index 4e1f982..5db30c9 100644 --- a/src/lib/cards/user/UserListPreAuthKeys.svelte +++ b/src/lib/cards/user/UserListPreAuthKeys.svelte @@ -8,8 +8,11 @@ import { slide } from 'svelte/transition'; import { createPreAuthKey } from '$lib/common/api'; import { debug } from '$lib/common/debug'; + import { isValidTag, toastError } from '$lib/common/funcs'; + import { InputChip, getToastStore } from '@skeletonlabs/skeleton'; import CardSeparator from '../CardSeparator.svelte'; import { App } from '$lib/States.svelte'; + import { _ } from 'svelte-i18n'; type UserListPreAuthKeysProps = { user: User, @@ -17,19 +20,31 @@ } let { user = $bindable(), - title = 'PreAuth Keys:', + title = $_('users.preAuthKeys') + ':', }: UserListPreAuthKeysProps = $props(); + const ToastStore = getToastStore(); let hideInvalid = $state(true); let showCreate = $state(false); let disableCreate = $state(false); let checked = $state(defaultChecked()); let expires = $state(defaultExpires()); + let tags = $state([] as string[]); const preAuthKeys = $derived( App.preAuthKeys.value.filter((p) => { - return (p.user.id === user.id) - && (!hideInvalid || (hideInvalid && !isExpiredOrUsed(p))); + // Ensure this key belongs to the current user + if (p.user.id !== user.id) { + return false; + } + + // If hideInvalid is false, show all keys + if (!hideInvalid) { + return true; + } + + // If hideInvalid is true, filter out expired or single-use keys + return !isExpiredOrUsed(p); }) ); @@ -50,7 +65,18 @@ } function isExpiredOrUsed(p: PreAuthKey): boolean { - return new Date() > new Date(p.expiration) || (p.used && !p.reusable); + // Safely check expiration - some old format keys might have issues + try { + if (!p.expiration) { + // If no expiration, consider it valid (never expires) + return p.used && !p.reusable; + } + return new Date() > new Date(p.expiration) || (p.used && !p.reusable); + } catch (e) { + debug('Error checking expiration for key', p.id, e); + // If there's an error checking expiration, don't hide the key + return false; + } }; @@ -58,7 +84,7 @@
-

Hide Invalid

+

{$_('cards.hideInvalid')}

{#if showCreate}
-
-
+ +
+ +
+ + +
+ + +
+ +
+ { + toastError($_('deploy.tagError'), ToastStore); + }} + /> +
+
+
+ + +
+ +
+ + +
+ + +
+ - -
-
- -
-
- -
diff --git a/src/lib/cards/user/UserProvider.svelte b/src/lib/cards/user/UserProvider.svelte index e18e23b..2059e45 100644 --- a/src/lib/cards/user/UserProvider.svelte +++ b/src/lib/cards/user/UserProvider.svelte @@ -1,6 +1,7 @@ - - {user.provider || 'local'} + + {user.provider || $_('cards.local')} \ No newline at end of file diff --git a/src/lib/cards/user/UserTileCard.svelte b/src/lib/cards/user/UserTileCard.svelte index 3f4b2c9..72db0c0 100644 --- a/src/lib/cards/user/UserTileCard.svelte +++ b/src/lib/cards/user/UserTileCard.svelte @@ -6,6 +6,7 @@ import CardTileContainer from '../CardTileContainer.svelte'; import OnlineUserIndicator from '$lib/parts/OnlineUserIndicator.svelte'; import { App } from '$lib/States.svelte'; + import { _ } from 'svelte-i18n'; type UserTileCardProps = { user: User, @@ -25,20 +26,20 @@
- ID: {user.id} + {$_('common.id')}: {user.id}
{getUserDisplay(user)}
-
Created:
+
{$_('cards.created')}
{dateToStr(new Date(user.createdAt))}
-
Nodes:
+
{$_('cards.nodes')}
{nodeCount}
diff --git a/src/lib/common/acl.svelte.ts b/src/lib/common/acl.svelte.ts index 3bb281d..69546e3 100644 --- a/src/lib/common/acl.svelte.ts +++ b/src/lib/common/acl.svelte.ts @@ -12,8 +12,8 @@ export type AclTagOwners = { [key: string]: TagOwners } export type AclHosts = { [key: string]: string } export type AclPolicies = AclPolicy[] export type AclSshRules = AclSshRule[] -export type AclPoliciesIndexed = {policy: AclPolicy, idx: number}[] -export type AclSshRulesIndexed = {rule: AclSshRule, idx: number}[] +export type AclPoliciesIndexed = { policy: AclPolicy, idx: number }[] +export type AclSshRulesIndexed = { rule: AclSshRule, idx: number }[] // metadata for ACL policy entries export type HAMeta = { @@ -72,7 +72,7 @@ export class ACLBuilder implements ACL { tagOwners = $state({}) hosts = $state({}) acls = $state([]) - ssh = $state(undefined) + ssh = $state(undefined) constructor( groups: AclGroups, @@ -112,26 +112,71 @@ export class ACLBuilder implements ACL { } static addPolicyMeta(policy: AclPolicy): boolean { - if (policy["#ha-meta"] === undefined){ - policy["#ha-meta"] = HAMetaDefault - } + if (policy["#ha-meta"] === undefined) { + policy["#ha-meta"] = HAMetaDefault + } return policy["#ha-meta"] !== undefined } static fromPolicy(acl: ACL | string): ACLBuilder { - if (typeof acl === "string"){ + if (typeof acl === "string") { return this.fromPolicy(JWCC.parse(acl)) } const ssh = acl.ssh ? [...acl.ssh] : [] - return new ACLBuilder( - {...acl.groups}, - {...acl.tagOwners}, - {...acl.hosts}, + const builder = new ACLBuilder( + { ...acl.groups }, + { ...acl.tagOwners }, + { ...acl.hosts }, [...acl.acls], [...ssh], ) + builder.normalize() + return builder + } + + // Ensures all user identifiers contain an '@' to satisfy Headscale's Go v2 parser requirements. + // This is especially important for local users like 'echo' which must be 'echo@'. + normalize() { + const hostNames = new Set(Object.keys(this.hosts)); + const isHost = (id: string) => hostNames.has(id); + + // Normalize Tag Owners + for (const tag in this.tagOwners) { + this.tagOwners[tag] = this.tagOwners[tag].map(id => ACLBuilder.normalizeIdentifier(id, false)); + } + + // Normalize ACL Policies + for (const acl of this.acls) { + acl.src = acl.src.map(id => ACLBuilder.normalizeIdentifier(id, isHost(id))); + acl.dst = acl.dst.map(dst => { + const hostPart = ACLBuilder.getPolicyDstHost(dst); + const ports = ACLBuilder.getPolicyDstPorts(dst); + if (hostPart === dst) { // No ports specified + return ACLBuilder.normalizeIdentifier(dst, isHost(dst)); + } + return ACLBuilder.normalizeIdentifier(hostPart, isHost(hostPart)) + ":" + ports; + }); + } + + // Normalize SSH Rules + if (this.ssh) { + for (const rule of this.ssh) { + rule.src = rule.src.map(id => ACLBuilder.normalizeIdentifier(id, isHost(id))); + rule.dst = rule.dst.map(id => ACLBuilder.normalizeIdentifier(id, isHost(id))); + // Note: rule.users are Unix usernames, they don't need @ + } + } + } + + private static normalizeIdentifier(id: string, isHost: boolean): string { + if (!id || id === "*" || id.includes("@") || id.includes("/") || id.startsWith("autogroup:") || ACLBuilder.getPrefix(id) !== null || isValidIP(id) || isHost) { + return id; + } + // If it's a bare name (not group:, not tag:, no @, not wildcard, not CIDR, not IP, not a defined host), it's a user. + // Headscale/Tailscale Go v2 parser requires an @ for user identifiers. + return id + "@"; } private static getPrefix(name: string): PrefixType | null { @@ -164,11 +209,11 @@ export class ACLBuilder implements ACL { return { prefixed, stripped } } - static normalizeTag(tag: string): {prefixed: string, stripped: string} { + static normalizeTag(tag: string): { prefixed: string, stripped: string } { return ACLBuilder.normalizePrefix(tag, "tag") } - static normalizeGroup(group: string): {prefixed: string, stripped: string} { + static normalizeGroup(group: string): { prefixed: string, stripped: string } { return ACLBuilder.normalizePrefix(group, "group") } @@ -203,10 +248,10 @@ export class ACLBuilder implements ACL { } static validateHostValue(value: string): string { - if(isValidIP(value)) { + if (isValidIP(value)) { return value } - if(isValidCIDR(value)) { + if (isValidCIDR(value)) { return value } throw new Error("Invalid Host IP or CIDR") @@ -232,7 +277,7 @@ export class ACLBuilder implements ACL { createHost(name: string, cidr: string) { - if(this.getHostCIDR(name) !== undefined) { + if (this.getHostCIDR(name) !== undefined) { throw new Error(`host "${name}" already exists`) } this.setHost(name, cidr) @@ -259,7 +304,7 @@ export class ACLBuilder implements ACL { } const hosts: AclHosts = {} - Object.entries(this.hosts).forEach(([name, value])=>{ + Object.entries(this.hosts).forEach(([name, value]) => { hosts[name === nameOld ? nameNew : name] = value }) this.hosts = hosts @@ -292,7 +337,7 @@ export class ACLBuilder implements ACL { } // remove group from SSH - if (this.ssh !== undefined){ + if (this.ssh !== undefined) { for (const ssh of this.ssh) { ssh.src = ssh.src.filter(s => s !== name) ssh.dst = ssh.dst.filter(d => d !== name) @@ -356,18 +401,19 @@ export class ACLBuilder implements ACL { const { prefixed } = ACLBuilder.normalizePrefix(name, 'tag') const ownersAll = [...owners] this.tagOwners[prefixed] = ownersAll + this.normalize() } getTagNames(withPrefix: boolean = false): string[] { return Object.keys(this.tagOwners).map(name => { - let {stripped, prefixed} = ACLBuilder.normalizePrefix(name, 'tag') + let { stripped, prefixed } = ACLBuilder.normalizePrefix(name, 'tag') return withPrefix ? prefixed : stripped }) } - getTagOwners(name: string): string[]{ + getTagOwners(name: string): string[] { const { stripped, prefixed } = ACLBuilder.normalizePrefix(name, 'tag') - const owners = this.tagOwners[prefixed] + const owners = this.tagOwners[prefixed] if (owners === undefined) { throw new Error(`Tag ${stripped} does not exist`) } @@ -414,13 +460,13 @@ export class ACLBuilder implements ACL { } // remove tag from ACLs - for (const acl of this.acls){ + for (const acl of this.acls) { acl.src = acl.src.filter(s => s !== prefixed); acl.dst = acl.dst.filter(d => d !== prefixed); } // remove tag from SSH - if (this.ssh !== undefined){ + if (this.ssh !== undefined) { for (const ssh of this.ssh) { ssh.src = ssh.src.filter(s => s !== prefixed) ssh.dst = ssh.dst.filter(d => d !== prefixed) @@ -495,6 +541,7 @@ export class ACLBuilder implements ACL { const { prefixed } = ACLBuilder.normalizePrefix(name, 'group') this.groups[prefixed] = [...members] + this.normalize() } getGroupByName(name: string): string[] { @@ -541,7 +588,7 @@ export class ACLBuilder implements ACL { } // remove group from SSH policies - if (this.ssh !== undefined){ + if (this.ssh !== undefined) { for (const ssh of this.ssh) { ssh.src = ssh.src.filter(s => s !== prefixed) } @@ -562,15 +609,15 @@ export class ACLBuilder implements ACL { * setPolicyProto(idx, proto) */ - public static getPolicyDstHost(dst: string): string { - const i = dst.lastIndexOf(':') - return i < 0 ? dst : dst.substring(0, i) - } + public static getPolicyDstHost(dst: string): string { + const i = dst.lastIndexOf(':') + return i < 0 ? dst : dst.substring(0, i) + } - public static getPolicyDstPorts(dst: string): string { - const i = dst.lastIndexOf(':') - return i < 0 ? dst : dst.substring(i+1, dst.length) - } + public static getPolicyDstPorts(dst: string): string { + const i = dst.lastIndexOf(':') + return i < 0 ? dst : dst.substring(i + 1, dst.length) + } createPolicy(policy: AclPolicy) { @@ -611,19 +658,21 @@ export class ACLBuilder implements ACL { "#ha-meta": HAMetaDefault, action: "accept", proto: undefined, - src: [ "*" ], - dst: [ "*:*" ], + src: ["*"], + dst: ["*:*"], } } setPolicySrc(idx: number, src: string[]) { this.validatePolicyIndex(idx) this.acls[idx].src = src + this.normalize() } setPolicyDst(idx: number, dst: string[]) { this.validatePolicyIndex(idx) this.acls[idx].dst = dst + this.normalize() } setPolicyProto(idx: number, proto: string | undefined) { @@ -660,19 +709,19 @@ export class ACLBuilder implements ACL { */ createSshRule(rule: AclSshRule) { - if (this.ssh === undefined){ + if (this.ssh === undefined) { this.ssh = [] } this.ssh.push(rule) } - getAllSshRules(): AclSshRules|undefined { + getAllSshRules(): AclSshRules | undefined { return this.ssh } getSshRule(idx: number): AclSshRule { this.validateSshRuleIndex(idx) - if (this.ssh !== undefined){ + if (this.ssh !== undefined) { return this.ssh[idx] } throw new Error("No SSH Rules defined") @@ -694,17 +743,18 @@ export class ACLBuilder implements ACL { } public static getPolicyTitle(pol: AclPolicy, idx: number): string { - const pfx = "#" + (idx + 1) + ": " - if (pol["#ha-meta"] === undefined || pol["#ha-meta"].name === "") { - return pfx + "Policy #" + (idx + 1) - } - return pfx + pol["#ha-meta"].name - } + const pfx = "#" + (idx + 1) + ": " + if (pol["#ha-meta"] === undefined || pol["#ha-meta"].name === "") { + return pfx + "Policy #" + (idx + 1) + } + return pfx + pol["#ha-meta"].name + } setSshRuleSrc(idx: number, src: string[]) { this.validateSshRuleIndex(idx) if (this.ssh != undefined) { this.ssh[idx].src = src + this.normalize() } } @@ -712,6 +762,7 @@ export class ACLBuilder implements ACL { this.validateSshRuleIndex(idx) if (this.ssh !== undefined) { this.ssh[idx].dst = dst + this.normalize() } } @@ -742,25 +793,28 @@ export class ACLBuilder implements ACL { } } -export async function saveConfig(acl: ACLBuilder, ToastStore: ToastStore, loading?: {setLoadingTrue: ()=>void, setLoadingFalse: ()=>void}) { - if(loading !== undefined){ +import { get } from 'svelte/store'; +import { _ } from 'svelte-i18n'; + +export async function saveConfig(acl: ACLBuilder, ToastStore: ToastStore, loading?: { setLoadingTrue: () => void, setLoadingFalse: () => void }) { + if (loading !== undefined) { loading.setLoadingTrue() } //loading = true try { await setPolicy(acl) - if(ToastStore !== undefined){ - toastSuccess('Saved ACL Configuration', ToastStore) + if (ToastStore !== undefined) { + toastSuccess(get(_)( 'acls.configSaved' ), ToastStore) } - } catch(err) { - if (err instanceof Error){ - if(ToastStore !== undefined){ + } catch (err) { + if (err instanceof Error) { + if (ToastStore !== undefined) { toastError('', ToastStore, err) } } debug(err) } finally { - if(loading !== undefined){ + if (loading !== undefined) { loading.setLoadingFalse() } } diff --git a/src/lib/common/api/base.ts b/src/lib/common/api/base.ts index 42214b8..bca9cec 100644 --- a/src/lib/common/api/base.ts +++ b/src/lib/common/api/base.ts @@ -59,7 +59,17 @@ function headers(): { headers: HeadersInit } { } export function toUrl(path: string): string { - return new URL(path, App.apiUrl.value).href + let base = App.apiUrl.value; + if (!base && typeof window !== 'undefined') { + base = window.location.origin; + } + if (!base.endsWith('/')) { + base += '/'; + } + if (path.startsWith('/')) { + path = path.substring(1); + } + return new URL(path, base).href; } async function apiFetch(path: string, init?: RequestInit, verbose: boolean = false): Promise { diff --git a/src/lib/common/api/create.ts b/src/lib/common/api/create.ts index 7666859..f3c0577 100644 --- a/src/lib/common/api/create.ts +++ b/src/lib/common/api/create.ts @@ -10,13 +10,14 @@ import { import { debug } from '$lib/common/debug'; import { API_URL_APIKEY, API_URL_NODE, API_URL_PREAUTHKEY, API_URL_USER } from './url'; -export async function createApiKey() { - // create 90-day API Key +export async function createApiKey(expirationDays?: number) { + // create API Key with custom expiration (default: 90 days) + const days = expirationDays ?? 90; const date = new Date(); - date.setDate(date.getDate() + 90); - const data = { expiration: date.toISOString() }; + date.setDate(date.getDate() + days); + const data = days > 0 ? { expiration: date.toISOString() } : {}; // no expiration if days <= 0 const { apiKey } = await apiPost(API_URL_APIKEY, data); - debug('Created API Key "...' + apiKey.slice(-10) + '"') + debug('Created API Key "...' + apiKey.slice(-10) + '" (expires in ' + days + ' days)') return apiKey; } @@ -42,12 +43,14 @@ export async function createPreAuthKey( ephemeral: boolean, reusable: boolean, expiration: Date | string, + tags: string[] = [], ) { const data = { user: user.id, reusable, ephemeral, expiration: new Date(expiration).toISOString(), + aclTags: tags.map((tag) => (tag.startsWith('tag:') ? tag : 'tag:' + tag)), }; const { preAuthKey } = await apiPost(API_URL_PREAUTHKEY, data); debug('Created PreAuthKey for user "' + user.name + '"'); diff --git a/src/lib/common/api/delete.ts b/src/lib/common/api/delete.ts index 0ea18a7..3e05210 100644 --- a/src/lib/common/api/delete.ts +++ b/src/lib/common/api/delete.ts @@ -4,6 +4,20 @@ import { debug } from '../debug'; import { API_URL_APIKEY, API_URL_NODE, API_URL_USER } from './url'; import { App } from '$lib/States.svelte'; +/** + * Extract raw prefix from formatted prefix returned by headscale API + * - New format (12-char): "hskey-api-XXXXXXXXXXXX-***" -> "XXXXXXXXXXXX" + * - Legacy format (7-char): "XXXXXXX***" -> "XXXXXXX" + */ +function extractRawPrefix(formattedPrefix: string): string { + // Remove "hskey-api-" prefix and "-***" suffix for new format + if (formattedPrefix.startsWith('hskey-api-')) { + return formattedPrefix.replace('hskey-api-', '').replace('-***', ''); + } + // Remove "***" suffix for legacy format + return formattedPrefix.replace('***', ''); +} + export async function expireApiKey(apiKey: string) { if (apiKey.indexOf('.') > -1) { apiKey = apiKey.split('.').at(0) || ''; @@ -12,8 +26,10 @@ export async function expireApiKey(apiKey: string) { debug('Invalid API Key/Prefix'); return; } + // Extract raw prefix if it's formatted + const rawPrefix = extractRawPrefix(apiKey); try { - await apiPost(`${API_URL_APIKEY}/expire`, { prefix: apiKey }); + await apiPost(`${API_URL_APIKEY}/expire`, { prefix: rawPrefix }); debug('Expired API Key with Prefix ' + apiKey); } catch (error) { debug(error); @@ -42,4 +58,24 @@ export async function deleteNode(node: Node): Promise { debug(error); return false; } +} + +export async function deleteApiKey(idOrPrefix: string | number): Promise { + try { + let path: string; + if (typeof idOrPrefix === 'number') { + // Delete by ID using query parameter + path = `${API_URL_APIKEY}?id=${idOrPrefix}`; + } else { + // Extract raw prefix from formatted prefix before deleting + const rawPrefix = extractRawPrefix(idOrPrefix); + path = `${API_URL_APIKEY}/${rawPrefix}`; + } + await apiDelete(path); + debug('Deleted API Key: ' + idOrPrefix); + return true; + } catch (error) { + debug(error); + return false; + } } \ No newline at end of file diff --git a/src/lib/common/api/get.ts b/src/lib/common/api/get.ts index 0b2936e..2f260fc 100644 --- a/src/lib/common/api/get.ts +++ b/src/lib/common/api/get.ts @@ -1,5 +1,7 @@ -import { API_URL_NODE, API_URL_POLICY, API_URL_PREAUTHKEY, API_URL_USER, apiGet } from '$lib/common/api'; +import { API_URL_APIKEY, API_URL_NODE, API_URL_POLICY, API_URL_PREAUTHKEY, API_URL_USER, apiGet } from '$lib/common/api'; import type { + ApiApiKeys, + ApiKey, ApiNodes, ApiPolicy, ApiPreAuthKeys, @@ -9,6 +11,7 @@ import type { User, } from '$lib/common/types'; import { debug } from '../debug'; +import { mapApiPreAuthKeys } from './mappers'; export async function getPreAuthKeys( user_ids?: string[], @@ -17,23 +20,52 @@ export async function getPreAuthKeys( if (user_ids == undefined) { user_ids = (await getUsers(init)).map((u) => u.id); } - const promises: Promise[] = []; - let preAuthKeysAll: PreAuthKey[] = []; - user_ids.forEach(async (user_id: string) => { + // Fetch all users first to have User objects for mapping + const allUsers = await getUsers(init); + const userMap = new Map(allUsers.map(u => [u.id, u])); + + const promises: Promise[] = []; + const userIdList: string[] = []; + + user_ids.forEach((user_id: string) => { if(user_id != ""){ promises.push( apiGet(API_URL_PREAUTHKEY + '?user=' + user_id, init), ); + userIdList.push(user_id); } }); - promises.forEach(async (p) => { - const { preAuthKeys } = await p; - preAuthKeysAll = preAuthKeysAll.concat(preAuthKeys); + const results = await Promise.all(promises); + let preAuthKeysAll: PreAuthKey[] = []; + + results.forEach((data, index) => { + if (data && data.preAuthKeys && Array.isArray(data.preAuthKeys)) { + const userId = userIdList[index]; + const user = userMap.get(userId); + + if (user) { + try { + const mappedKeys = mapApiPreAuthKeys(data.preAuthKeys, user); + preAuthKeysAll = preAuthKeysAll.concat(mappedKeys); + debug(`Mapped ${mappedKeys.length} PreAuthKeys for user ${userId}`); + } catch (e) { + debug('Error mapping PreAuthKeys for user', userId, e); + } + } + } + }); + + // Remove duplicates based on ID + const seenIds = new Set(); + preAuthKeysAll = preAuthKeysAll.filter(item => { + const duplicate = seenIds.has(item.id); + seenIds.add(item.id); + return !duplicate; }); - await Promise.all(promises); + debug(`Total PreAuthKeys loaded: ${preAuthKeysAll.length}`); return preAuthKeysAll; } @@ -64,7 +96,24 @@ export async function getNodes(): Promise { return nodes; } +export async function getNode(nodeId: string | number): Promise { + const { node } = await apiGet<{ node: Node }>(`${API_URL_NODE}/${nodeId}`); + debug('Fetched Node ID:', nodeId); + return node; +} + export async function getPolicy(): Promise { const { policy } = await apiGet(API_URL_POLICY) return policy } + +export async function getApiKeys(init?: RequestInit): Promise { + const { apiKeys } = await apiGet(API_URL_APIKEY, init); + return apiKeys; +} + +export async function getHealth(): Promise<{ databaseConnectivity: boolean }> { + const { databaseConnectivity } = await apiGet<{ databaseConnectivity: boolean }>('/api/v1/health'); + debug('Health check - DB connectivity:', databaseConnectivity); + return { databaseConnectivity }; +} diff --git a/src/lib/common/api/mappers.ts b/src/lib/common/api/mappers.ts new file mode 100644 index 0000000..a53ac0e --- /dev/null +++ b/src/lib/common/api/mappers.ts @@ -0,0 +1,73 @@ +/** + * API Response Mappers + * + * Converts API responses to frontend types with proper handling of legacy/new formats + * and missing fields. + */ + +import { PreAuthKey, type User } from '$lib/common/types'; +import { debug } from '../debug'; + +/** + * Maps raw API response to PreAuthKey object + * Handles both new and legacy (old format) pre-auth keys + */ +export function mapApiPreAuthKey(data: any, user: User): PreAuthKey { + // Handle snake_case from gRPC-Gateway + const id = data.id || data.ID || ''; + const key = data.key || data.Key || ''; + const reusable = data.reusable ?? data.Reusable ?? false; + const ephemeral = data.ephemeral ?? data.Ephemeral ?? false; + const used = data.used ?? data.Used ?? false; + const expiration = data.expiration || data.Expiration || ''; + const createdAt = data.createdAt || data.CreatedAt || data.created_at || ''; + const aclTags = data.aclTags || data.AclTags || data.acl_tags || []; + + // Validate required fields + if (!id || !key) { + debug('Invalid PreAuthKey data:', { id, key }); + throw new Error('Invalid PreAuthKey: missing id or key'); + } + + // Create PreAuthKey instance + const pak = new PreAuthKey( + user, + String(id), + key, + Boolean(reusable), + Boolean(ephemeral), + Boolean(used), + String(expiration), + String(createdAt), + Array.isArray(aclTags) ? aclTags : [] + ); + + debug('Mapped PreAuthKey:', { + id: pak.id, + keyLen: pak.key.length, + keyPreview: pak.key.substring(0, 20), + reusable: pak.reusable, + ephemeral: pak.ephemeral, + used: pak.used, + isLegacy: !pak.key.includes('*') && !pak.key.startsWith('hskey-auth-'), + hasExpiration: !!pak.expiration + }); + + return pak; +} + +/** + * Maps raw API response list to PreAuthKey array + */ +export function mapApiPreAuthKeys(dataList: any[], user: User): PreAuthKey[] { + return dataList + .map(data => { + try { + return mapApiPreAuthKey(data, user); + } catch (e) { + debug('Failed to map PreAuthKey:', data, e); + return null; + } + }) + .filter((pak): pak is PreAuthKey => pak !== null); +} diff --git a/src/lib/common/api/modify.ts b/src/lib/common/api/modify.ts index d5e176a..9bb3de5 100644 --- a/src/lib/common/api/modify.ts +++ b/src/lib/common/api/modify.ts @@ -30,22 +30,25 @@ export async function renameNode(n: Node, nameNew: string): Promise { } export async function changeNodeOwner(n: Node, newUserID: string): Promise { + // Headscale v0.28 behavior: if node is tagged, user must re-auth on client side. + // We'll keep the API call but handle the 404/Error more gracefully or provide context. const path = `${API_URL_NODE}/${n.id}/user`; - const { node } = await apiPost(path, {user: newUserID}); + const { node } = await apiPost(path, { user: newUserID }); debug('Re-assigned Node from "' + n.user.name + '" to "' + node.user.name + '"'); return node; } export async function expirePreAuthKey(pak: PreAuthKey) { const path = `${API_URL_PREAUTHKEY}/expire`; - const data = { user: pak.user.id, key: pak.key }; + const data = { id: pak.id }; await apiPost(path, data); } -export async function expireNode(n: Node): Promise { +export async function expireNode(n: Node, date?: string): Promise { const path = `${API_URL_NODE}/${n.id}/expire`; - const { node } = await apiPost(path, undefined); - debug('Expired Node "' + n.givenName + '"'); + const body = date ? { expiry: date } : undefined; + const { node } = await apiPost(path, body); + debug('Expired Node "' + n.givenName + '"' + (date ? ' at ' + date : '')); return node; } @@ -108,3 +111,10 @@ export async function refreshApiKey() { App.apiKeyInfo.value.informedExpiringSoon = false App.apiKeyInfo.value.informedUnauthorized = false } + +export async function backfillNodeIPs(confirmed: boolean = false): Promise { + const path = `${API_URL_NODE}/backfillips`; + const { changes } = await apiPost<{ changes: string[] }>(path, { confirmed }); + debug('BackfillNodeIPs:', confirmed ? 'Applied' : 'Dry run', '- Changes:', changes.length); + return changes; +} diff --git a/src/lib/common/debug.ts b/src/lib/common/debug.ts index 5fd8a9d..57908c6 100644 --- a/src/lib/common/debug.ts +++ b/src/lib/common/debug.ts @@ -1,6 +1,6 @@ import { App } from "$lib/States.svelte"; -export const version = '0.26.0'; +export const version = '0.28.0'; export function debug(...data: unknown[]) { // output if console debugging is enabled diff --git a/src/lib/common/errors.ts b/src/lib/common/errors.ts index 36b11ce..3322ec4 100644 --- a/src/lib/common/errors.ts +++ b/src/lib/common/errors.ts @@ -3,6 +3,8 @@ import { informUserUnauthorized } from '$lib/States.svelte'; import type { ToastStore } from '@skeletonlabs/skeleton'; import { debug } from './debug'; +import { _ } from 'svelte-i18n'; +import { get } from 'svelte/store'; export class ApiAuthError extends Error { constructor() { @@ -23,3 +25,22 @@ export function createPopulateErrorHandler(ToastStore: ToastStore) { debug('Error Handler:', err); }; } + +export function localizeError(err: unknown): string { + const errorMsg = err instanceof Error ? err.message : String(err); + const translate = get(_); + + if (errorMsg.includes('cannot remove all tags from a node')) { + return translate('cards.cannotRemoveAllTags'); + } + + if (errorMsg.includes('re-authenticating with') || errorMsg.includes('force-reauth')) { + return translate('cards.taggedToUserReauthRequired'); + } + + if (errorMsg.includes('are invalid or not permitted')) { + return translate('cards.tagsNotPermitted'); + } + + return errorMsg; +} diff --git a/src/lib/common/export.ts b/src/lib/common/export.ts new file mode 100644 index 0000000..5916aa4 --- /dev/null +++ b/src/lib/common/export.ts @@ -0,0 +1,257 @@ +import type { Node, User, PreAuthKey, ApiKey } from './types'; +import { getPolicy } from './api'; +import { App } from '$lib/States.svelte'; + +export type ExportFormat = 'json' | 'csv' | 'yaml'; +export type ExportResource = 'users' | 'nodes' | 'preAuthKeys' | 'apiKeys' | 'policy'; + +export type ExportOptions = { + format: ExportFormat; + resources: ExportResource[]; + includeMetadata?: boolean; +}; + +function downloadFile(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} + +export function downloadJSON(data: any, filename: string) { + const json = JSON.stringify(data, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + downloadFile(blob, filename); +} + +export function downloadCSV(data: any[], filename: string) { + if (data.length === 0) { + return; + } + + // Extract all unique keys from all objects + const keys = Array.from( + new Set(data.flatMap((item) => Object.keys(item))) + ); + + // CSV header + const csvHeader = keys.join(','); + + // CSV rows + const csvRows = data.map((item) => + keys + .map((key) => { + const value = item[key]; + // Handle null/undefined + if (value === null || value === undefined) { + return ''; + } + // Convert to string + const strValue = String(value); + // Escape special characters (quotes, commas, newlines) + if ( + strValue.includes(',') || + strValue.includes('"') || + strValue.includes('\n') + ) { + return `"${strValue.replace(/"/g, '""')}"`; + } + return strValue; + }) + .join(',') + ); + + const csv = [csvHeader, ...csvRows].join('\n'); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + downloadFile(blob, filename); +} + +// Flatten nested objects for CSV export +function flattenNode(node: Node) { + return { + id: node.id, + name: node.name, + givenName: node.givenName, + user: node.user.name, + ipAddresses: node.ipAddresses.join('; '), + online: node.online, + lastSeen: node.lastSeen || '', + createdAt: node.createdAt, + expiry: node.expiry || '', + registerMethod: node.registerMethod, + tags: node.tags.join('; '), + approvedRoutes: node.approvedRoutes.join('; '), + availableRoutes: node.availableRoutes.join('; '), + subnetRoutes: node.subnetRoutes.join('; '), + }; +} + +function flattenUser(user: User) { + return { + id: user.id, + name: user.name, + displayName: user.displayName || '', + email: user.email || '', + createdAt: user.createdAt, + provider: user.provider || '', + providerId: user.providerId || '', + }; +} + +function flattenPreAuthKey(key: PreAuthKey) { + return { + id: key.id, + key: key.key, + user: key.user.name, + reusable: key.reusable, + ephemeral: key.ephemeral, + used: key.used, + expiration: key.expiration, + createdAt: key.createdAt, + aclTags: key.aclTags.join('; '), + }; +} + +function flattenApiKey(key: ApiKey) { + return { + id: key.id, + prefix: key.prefix, + createdAt: key.createdAt, + lastSeen: key.lastSeen || '', + expiration: key.expiration || '', + }; +} + +export async function exportHeadscaleData(options: ExportOptions) { + const exportData: any = {}; + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); + + if (options.includeMetadata) { + exportData.metadata = { + exportedAt: new Date().toISOString(), + apiUrl: App.apiUrl.value, + format: options.format, + resources: options.resources, + }; + } + + // Collect data for each resource + if (options.resources.includes('users')) { + exportData.users = App.users.value; + } + + if (options.resources.includes('nodes')) { + exportData.nodes = App.nodes.value; + } + + if (options.resources.includes('preAuthKeys')) { + exportData.preAuthKeys = App.preAuthKeys.value; + } + + if (options.resources.includes('apiKeys')) { + // API Keys need to be fetched separately if not already in App state + // For now, we'll skip this or require it to be fetched beforehand + // exportData.apiKeys = await getApiKeys(); + } + + if (options.resources.includes('policy')) { + try { + exportData.policy = await getPolicy(); + } catch (error) { + console.error('Failed to export policy:', error); + } + } + + const filename = `headscale-export-${timestamp}`; + + switch (options.format) { + case 'json': + downloadJSON(exportData, `${filename}.json`); + break; + + case 'csv': + // CSV needs separate files for each resource type + if (options.resources.includes('users') && exportData.users) { + downloadCSV( + exportData.users.map(flattenUser), + `${filename}-users.csv` + ); + } + if (options.resources.includes('nodes') && exportData.nodes) { + downloadCSV( + exportData.nodes.map(flattenNode), + `${filename}-nodes.csv` + ); + } + if (options.resources.includes('preAuthKeys') && exportData.preAuthKeys) { + downloadCSV( + exportData.preAuthKeys.map(flattenPreAuthKey), + `${filename}-preAuthKeys.csv` + ); + } + if (options.resources.includes('apiKeys') && exportData.apiKeys) { + downloadCSV( + exportData.apiKeys.map(flattenApiKey), + `${filename}-apiKeys.csv` + ); + } + // Policy is not suitable for CSV export + break; + + case 'yaml': + // YAML export would require additional library + // For now, we'll just export as JSON + downloadJSON(exportData, `${filename}.yaml`); + break; + } +} + +// Quick export functions for individual resources +export function exportUsers(format: 'json' | 'csv' = 'json') { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); + const filename = `headscale-users-${timestamp}`; + + if (format === 'json') { + downloadJSON(App.users.value, `${filename}.json`); + } else { + downloadCSV(App.users.value.map(flattenUser), `${filename}.csv`); + } +} + +export function exportNodes(format: 'json' | 'csv' = 'json') { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); + const filename = `headscale-nodes-${timestamp}`; + + if (format === 'json') { + downloadJSON(App.nodes.value, `${filename}.json`); + } else { + downloadCSV(App.nodes.value.map(flattenNode), `${filename}.csv`); + } +} + +export function exportPreAuthKeys(format: 'json' | 'csv' = 'json') { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); + const filename = `headscale-preAuthKeys-${timestamp}`; + + if (format === 'json') { + downloadJSON(App.preAuthKeys.value, `${filename}.json`); + } else { + downloadCSV(App.preAuthKeys.value.map(flattenPreAuthKey), `${filename}.csv`); + } +} + +export async function exportPolicy() { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); + const filename = `headscale-policy-${timestamp}.json`; + + try { + const policy = await getPolicy(); + // Policy is already a string (HuJSON format) + const blob = new Blob([policy], { type: 'application/json' }); + downloadFile(blob, filename); + } catch (error) { + console.error('Failed to export policy:', error); + } +} diff --git a/src/lib/common/funcs.ts b/src/lib/common/funcs.ts index 0ca9c2f..cc014d3 100644 --- a/src/lib/common/funcs.ts +++ b/src/lib/common/funcs.ts @@ -4,6 +4,8 @@ import IPAddr from 'ipaddr.js'; import { debug } from './debug'; import DOMPurify from 'dompurify'; import type { Direction, Node, OnlineStatus, User } from './types'; +import { get } from 'svelte/store'; +import { locale as i18nLocale, _ } from 'svelte-i18n'; import { App } from '$lib/States.svelte'; export function clone(item: T): T { @@ -16,27 +18,35 @@ export function focus(el: HTMLElement | null) { } } +export function getUserAclName(user: User): string { + const name = user.email ? user.email : user.name; + if (name && !name.includes('@')) { + return name + '@'; + } + return name; +} + export function arraysEqual(a: T[], b: T[]): boolean { - if(a.length !== b.length){ + if (a.length !== b.length) { return false } return JSON.stringify(a) == JSON.stringify(b) - if (a === b){ + if (a === b) { return true; } - if (a == null || b == null){ + if (a == null || b == null) { return false; } - + if (a.length !== b.length) { return false; } for (var i = 0; i < a.length; ++i) { - if (a[i] !== b[i]){ + if (a[i] !== b[i]) { return false; } } @@ -98,7 +108,7 @@ export function getTime( export function getTimeDifferenceMessage(time1: number): string { const difference = getTimeDifference(time1, new Date().getTime()); - return difference.finite ? difference.message : 'Does Not Expire'; + return difference.finite ? difference.message : (safeIsZh() ? '不过期' : 'Does Not Expire'); } export function getTimeDifferenceColor(td: TimeDifference): string { @@ -114,7 +124,7 @@ export function getTimeDifference(time1: number, time2?: number): TimeDifference return { future: true, finite: false, - message: 'Does Not Expire', + message: safeIsZh() ? '不过期' : 'Does Not Expire', }; } @@ -131,24 +141,40 @@ export function getTimeDifference(time1: number, time2?: number): TimeDifference const weeks = Math.floor(days / 7); const months = Math.floor(weeks / 4); - if (months > 0) { - message = `${months} month${months == 1 ? '' : 's'}`; - } else if (weeks > 0) { - message = `${weeks} week${weeks == 1 ? '' : 's'}`; - } else if (days > 0) { - message = `${days} day${days == 1 ? '' : 's'}`; - } else if (hours > 0) { - message = `${hours} hour${hours == 1 ? '' : 's'}`; - } else if (minutes > 0) { - message = `${minutes} minute${minutes == 1 ? '' : 's'}`; + if (safeIsZh()) { + if (months > 0) { + message = `${months}个月`; + } else if (weeks > 0) { + message = `${weeks}周`; + } else if (days > 0) { + message = `${days}天`; + } else if (hours > 0) { + message = `${hours}小时`; + } else if (minutes > 0) { + message = `${minutes}分钟`; + } else { + message = `${seconds}秒`; + } } else { - message = `${seconds} second${seconds == 1 ? '' : 's'}`; + if (months > 0) { + message = `${months} month${months == 1 ? '' : 's'}`; + } else if (weeks > 0) { + message = `${weeks} week${weeks == 1 ? '' : 's'}`; + } else if (days > 0) { + message = `${days} day${days == 1 ? '' : 's'}`; + } else if (hours > 0) { + message = `${hours} hour${hours == 1 ? '' : 's'}`; + } else if (minutes > 0) { + message = `${minutes} minute${minutes == 1 ? '' : 's'}`; + } else { + message = `${seconds} second${seconds == 1 ? '' : 's'}`; + } } return { future: isFuture, finite: true, - message: message + ` ${isFuture ? 'from now' : 'ago'}`, + message: safeIsZh() ? `${message}${isFuture ? '后' : '前'}` : message + ` ${isFuture ? 'from now' : 'ago'}`, }; } @@ -157,16 +183,34 @@ export function dateToStr(d: Date | string) { d = new Date(d); } - return d.toLocaleString('en-US', { + if (safeIsZh()) { + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const hour = String(d.getHours()).padStart(2, '0'); + const minute = String(d.getMinutes()).padStart(2, '0'); + return `${year}-${month}-${day} ${hour}:${minute}`; + } + + return d.toLocaleString('en-GB', { day: '2-digit', - month: '2-digit', + month: 'short', year: 'numeric', - hour: 'numeric', + hour: '2-digit', minute: '2-digit', - //hour12: false + hour12: false }); } +function safeIsZh(): boolean { + try { + const lang = get(i18nLocale) ?? ''; + return lang === 'zh' || lang.startsWith('zh-'); + } catch { + return false; + } +} + export function toastSuccess(message: string, toastStore: ToastStore) { message = DOMPurify.sanitize(message) toastStore.trigger({ @@ -200,18 +244,22 @@ export function toastError(message: string, toastStore: ToastStore, error?: Erro export function copyToClipboard( s: string, toastStore?: ToastStore, - toastMessage = 'Copied to Clipboard!', + toastMessage?: string, ) { + const translate = get(i18nLocale); // This is actually the locale string, I need the formatter + // Wait, let's use the same pattern as errors.ts + const msg = toastMessage ?? get(_)( 'common.copySuccess' ); + navigator.clipboard .writeText(s) .then(() => { if (toastStore != undefined) { - toastSuccess(toastMessage, toastStore); + toastSuccess(msg, toastStore); } }) .catch(() => { if (toastStore) { - toastError('Failed to copy to clipboard!', toastStore); + toastError(get(_)( 'common.copyFailed' ), toastStore); } }); } @@ -219,7 +267,7 @@ export function copyToClipboard( export function isValidTag(tag: string): boolean { // the only restrictions I could find were to be all lowercase, no-spaces // I made it alphanumeric with dashes and underscores only - return new RegExp(/^[a-z0-9-_]+$/, 'g').test(tag); + return new RegExp(/^[a-z0-9-_]+$/).test(tag); } function getInverseMask(prefix: number, bitsTotal: number, bitsPart: number = 8) { @@ -245,7 +293,7 @@ export function isValidIP(addr: string): boolean { try { IPAddr.parse(addr) return true - } catch(err) { + } catch (err) { debug(err) return false } @@ -295,7 +343,7 @@ function makeDrawerSettings( export function openDrawer(drawerStore: DrawerStore, id: string, meta: unknown) { drawerStore.open(makeDrawerSettings(id, meta)); } -export function toOptions(values: string[]): {label: string, value:string}[] { +export function toOptions(values: string[]): { label: string, value: string }[] { return values.map(v => ({ label: v, value: v, @@ -389,7 +437,7 @@ export function filterUser(user: User, filterString: string, onlineStatus: Onlin } export function filterNode(node: Node, filterString: string, onlineStatus: OnlineStatus = "all"): boolean { - if((onlineStatus === "online" && !node.online) || (onlineStatus === "offline" && node.online)){ + if ((onlineStatus === "online" && !node.online) || (onlineStatus === "offline" && node.online)) { return false } @@ -401,15 +449,14 @@ export function filterNode(node: Node, filterString: string, onlineStatus: Onlin const r = RegExp(filterString); const getTag = (tag: string) => { if (tag.startsWith('tag:')) { - return tag.substring(0, 4); + return tag.substring(4); } return tag; }; return ( r.test(node.name) || r.test(node.givenName) || - node.forcedTags.map(getTag).some((tag) => r.test(tag)) || - node.validTags.map(getTag).some((tag) => r.test(tag)) + node.tags.map(getTag).some((tag) => r.test(tag)) ); } catch (err) { return true; @@ -418,13 +465,13 @@ export function filterNode(node: Node, filterString: string, onlineStatus: Onlin export function getSortedFilteredUsers( users: User[], - filterString:string, + filterString: string, sortMethod: string, sortDirection: Direction, onlineStatus: OnlineStatus, -){ +) { return getSortedUsers( - users.filter((user)=> filterUser(user, filterString, onlineStatus)), + users.filter((user) => filterUser(user, filterString, onlineStatus)), sortMethod, sortDirection, ) @@ -432,18 +479,18 @@ export function getSortedFilteredUsers( export function getSortedFilteredNodes( nodes: Node[], - filterString:string, + filterString: string, sortMethod: string, sortDirection: Direction, onlineStatus: OnlineStatus, ignoreRouteless: boolean = false, -){ +) { let nodesSortedFiltered = getSortedNodes( - nodes.filter((node)=> filterNode(node, filterString, onlineStatus)), + nodes.filter((node) => filterNode(node, filterString, onlineStatus)), sortMethod, sortDirection, ) - if(ignoreRouteless === true){ + if (ignoreRouteless === true) { return nodesSortedFiltered.filter((n) => { return n.availableRoutes.length > 0; }) diff --git a/src/lib/common/themes.ts b/src/lib/common/themes.ts index 9a8f265..72d82f6 100644 --- a/src/lib/common/themes.ts +++ b/src/lib/common/themes.ts @@ -9,6 +9,7 @@ export const ALL_THEMES = [ 'hamlindigo', 'gold-nouveau', 'crimson', + 'decula', ]; export function setTheme(theme: string) { diff --git a/src/lib/common/types.ts b/src/lib/common/types.ts index b2843e5..81749ee 100644 --- a/src/lib/common/types.ts +++ b/src/lib/common/types.ts @@ -22,7 +22,21 @@ export type ExpirationMessage = { message: string; color: string; }; +export type SystemStats = { + totalUsers: number; + totalNodes: number; + onlineNodes: number; + offlineNodes: number; + totalRoutes: number; + enabledRoutes: number; + totalPreAuthKeys: number; + validPreAuthKeys: number; +}; +export type HealthStatus = { + databaseConnectivity: boolean; + lastChecked: Date; +}; export function isNamed(item: unknown): item is Named { if (item != null && typeof item === 'object') { return ( @@ -142,9 +156,7 @@ export type Node = { | 'REGISTER_METHOD_AUTH_KEY' | 'REGISTER_METHOD_CLI' | 'REGISTER_METHOD_OIDC'; - forcedTags: string[]; - invalidTags: string[]; - validTags: string[]; + tags: string[]; givenName: string; online: boolean; approvedRoutes: string[]; diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts new file mode 100644 index 0000000..ef6565d --- /dev/null +++ b/src/lib/i18n.ts @@ -0,0 +1,25 @@ +import { browser } from '$app/environment'; +import { init, register, locale } from 'svelte-i18n'; + +const defaultLocale = 'en'; + +register('en', () => import('./locales/en.json')); +register('zh', () => import('./locales/zh.json')); + +const getInitialLocale = () => { + if (browser) { + const stored = window.localStorage.getItem('locale'); + if (stored && (stored === 'en' || stored === 'zh')) { + return stored; + } + } + return defaultLocale; +}; + +init({ + fallbackLocale: defaultLocale, + initialLocale: getInitialLocale() +}); + +// Set the locale immediately for SSR compatibility +locale.set(getInitialLocale()); diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json new file mode 100644 index 0000000..062a6f8 --- /dev/null +++ b/src/lib/locales/en.json @@ -0,0 +1,319 @@ +{ + "app": { + "title": "Headscale-Admin", + "github": "GitHub" + }, + "navigation": { + "title": "Navigation", + "home": "Home", + "users": "Users", + "nodes": "Nodes", + "routes": "Routes", + "acls": "ACLs", + "status": "System Status", + "settings": "Settings", + "deploy": "Deploy" + }, + "settings": { + "title": "Settings", + "apiUrl": "API URL", + "apiKey": "API Key", + "apiUrlPlaceholder": "Enter API URL", + "apiKeyPlaceholder": "Enter your API Key", + "apiTtl": "API Refresh Interval (seconds)", + "theme": "Theme", + "language": "Language", + "debugging": "Console Debugging", + "save": "Save Settings", + "authorized": "Authorized", + "notAuthorized": "Not Authorized", + "expiresIn": "Expires in", + "checkingAuth": "Checking authorization...", + "savedSettings": "Saved Settings", + "logUsers": "Log Users", + "logNodes": "Log Nodes", + "logPreAuthKeys": "Log PreAuthKeys", + "logApiKeyInfo": "Log ApiKey Info", + "showApiKey": "Show API Key", + "hideApiKey": "Hide API Key", + "refreshApiKey": "Refresh API Key", + "rememberMe": "Remember API Key (Save to LocalStorage)", + "apiKeysManagement": "API Keys Management", + "apiKeysList": "API Keys List", + "createNewApiKey": "Create New API Key", + "expirationDays": "Expiration (Days)", + "neverExpire": "Never Expire", + "prefix": "Prefix", + "createdAt": "Created", + "lastSeen": "Last Seen", + "expiration": "Expiration", + "deleteApiKeyConfirm": "Are you sure you want to delete this API Key?", + "deleteApiKeyWarning": "This action cannot be undone. Any services using this key will lose access.", + "deletingCurrentKey": "Warning: You are about to delete the API Key currently in use!", + "apiKeyCreated": "API Key created successfully", + "apiKeyDeleted": "API Key deleted", + "loadingApiKeys": "Loading API Keys...", + "noApiKeys": "No API Keys found", + "unauthorizedMessage": "API Key is Unauthorized or Invalid", + "expiringSoonMessage": "API Key Expires Soon" + }, + "common": { + "close": "Close", + "cancel": "Cancel", + "confirm": "Confirm", + "delete": "Delete", + "edit": "Edit", + "save": "Save", + "create": "Create", + "loading": "Loading...", + "copyFailed": "Failed to copy to clipboard!", + "copySuccess": "Copied to Clipboard!", + "name": "Name", + "email": "Email", + "user": "User", + "node": "Node", + "route": "Route", + "status": "Status", + "actions": "Actions", + "online": "Online", + "offline": "Offline", + "expired": "Expired", + "active": "Active", + "createdAt": "Created At", + "lastSeen": "Last Seen", + "hostname": "Hostname", + "addresses": "Addresses", + "tags": "Tags", + "owner": "Owner", + "routes": "Routes", + "all": "All", + "id": "ID", + "sort": "Sort", + "filter": "Filter", + "search": "Search...", + "createUser": "Create User", + "createNode": "Create Node", + "newUsername": "New Username...", + "deviceKey": "Device Key...", + "usernameAndKeyRequired": "Username and Device Key are Required", + "createdUser": "Created user", + "createdNode": "Created node", + "failedCreateUser": "Failed to create user", + "failedCreateNode": "Failed to create node", + "export": "Export", + "exportData": "Export Data", + "exportFormat": "Format", + "exportResources": "Resources to Export", + "exportIncludeMetadata": "Include Metadata", + "exportSuccess": "Data exported successfully", + "exportFailed": "Failed to export data", + "exportAll": "Export All", + "users": "Users", + "nodes": "Nodes", + "preAuthKeys": "PreAuth Keys", + "apiKeys": "API Keys", + "policy": "Policy", + "batchOperations": "Batch Operations", + "selectAll": "Select All", + "deselectAll": "Deselect All", + "selectedCount": "{count} selected", + "batchDelete": "Batch Delete", + "batchSetTags": "Batch Set Tags", + "batchDeleteConfirm": "Confirm Batch Delete", + "batchDeleteWarning": "Are you sure you want to delete {count} node(s)? This action cannot be undone.", + "batchDeleted": "Successfully deleted {count} node(s)", + "enterTags": "Enter tags (comma-separated)", + "tagsUpdated": "Tags updated for {count} node(s)", + "backfillNodeIPs": "Backfill Node IPs", + "backfillNodeIPsDesc": "Assign IP addresses to nodes that don't have them yet", + "backfillConfirmDesc": "This will only fill missing IPs and will not modify existing node IPs. Review the changes below before applying.", + "dryRun": "Dry Run", + "applyChanges": "Apply Changes", + "backfillChanges": "Changes to be applied:", + "noChangesNeeded": "No changes needed", + "backfillCompleted": "Backfill completed: {count} change(s) applied" + }, + "status": { + "title": "System Status", + "healthCheck": "Health Check", + "databaseConnectivity": "Database Connectivity", + "healthy": "Healthy", + "unhealthy": "Unhealthy", + "lastChecked": "Last Checked", + "systemStatistics": "System Statistics", + "totalUsers": "Total Users", + "totalNodes": "Total Nodes", + "onlineNodes": "Online Nodes", + "offlineNodes": "Offline Nodes", + "totalRoutes": "Total Routes", + "enabledRoutes": "Enabled Routes", + "totalPreAuthKeys": "Total PreAuth Keys", + "validPreAuthKeys": "Valid PreAuth Keys", + "refreshing": "Refreshing...", + "refresh": "Refresh", + "autoRefresh": "Auto-refresh every 30s" + }, + "users": { + "title": "Users", + "createUser": "Create User", + "displayName": "Display Name", + "provider": "Provider", + "email": "Email", + "nodes": "Nodes", + "preAuthKeys": "Pre-Auth Keys" + }, + "nodes": { + "title": "Nodes", + "createNode": "Create Node", + "registrationMethod": "Registration Method", + "expiresAt": "Expires At", + "info": "Info" + }, + "routes": { + "title": "Routes", + "enabled": "Enabled", + "advertised": "Advertised", + "destination": "Destination" + }, + "acls": { + "title": "ACLs", + "config": "Config", + "groups": "Groups", + "hosts": "Hosts", + "policies": "Policies", + "sshRules": "SSH Rules", + "tagOwners": "Tag Owners", + "hostCreated": "Host '{name}' created", + "tagCreated": "Tag Ownership of '{name}' created", + "sshRuleCreated": "Created SSH Rule #{count}", + "policyCreated": "Created Policy #{count}", + "groupCreated": "Group '{name}' created", + "configLoaded": "Loaded ACL policy from server", + "configLoadFailed": "Unable to get ACL policy from server.", + "configSaved": "Saved ACL Configuration", + "createHost": "Create Host", + "createTag": "Create Tag", + "createSshRule": "Create SSH Rule", + "createPolicy": "Create Policy", + "createGroup": "Create Group", + "saveConfig": "Save Config", + "loadConfig": "Load Config", + "editConfig": "Edit Config", + "applyConfig": "Apply Config", + "cancelEditing": "Cancel Editing", + "resetConfig": "Reset Config" + }, + "deploy": { + "title": "Deploy", + "copyCommand": "Copied Command to Clipboard!", + "general": "General:", + "advertise": "Advertise:", + "accept": "Accept:", + "saveDefaults": "Save Defaults", + "savedDefaults": "Saved Deployment Defaults", + "shieldsUp": "Shields Up", + "shieldsUpHelp": "Block incoming connections", + "generateQR": "Generate QR Code", + "generateQRHelp": "Create a scannable QR code to import into TailScale client", + "reset": "Reset", + "resetHelp": "Reset unspecified settings to default values", + "operator": "Operator", + "operatorHelp": "(Unix Only) Run as a different user", + "forceReauth": "Force Reauthentication", + "forceReauthHelp": "Force user to re-authenticate to Headscale server", + "sshServer": "SSH Server", + "sshServerHelp": "Run a local SSH server accessible by administrators", + "preAuthKey": "PreAuth Key", + "preAuthKeyHelp": "A generated key to automatically authenticate the node for a given user", + "unattended": "Unattended", + "unattendedHelp": "Run the tailscale client in unattended mode (on startup)", + "allowLANAccess": "Allow LAN Access", + "allowLANAccessHelp": "Allow local network access while connected to the TailNet and using an exit node", + "advertiseExitNode": "Advertise Exit Node", + "advertiseExitNodeHelp": "Allow other nodes on the TailNet to use this node as a gateway", + "advertiseTags": "Advertise Tags", + "advertiseTagsHelp": "List of advertised tags to apply to a machine on provisioning", + "advertiseRoutes": "Advertise Routes", + "advertiseRoutesHelp": "List of subnets which are reachable via this node", + "acceptDNS": "Accept DNS", + "acceptDNSHelp": "Accept the HeadScale-provided DNS settings", + "acceptRoutes": "Accept Routes", + "acceptRoutesHelp": "Accept other nodes' advertised subnets", + "exitNode": "Exit Node", + "exitNodeHelp": "Use this node as a gateway (target node must advertise exit node)", + "validKeys": "Valid Key(s)", + "tagError": "Tag should be a lowercase alphanumeric word", + "cidrError": "Invalid CIDR Format" + }, + "cards": { + "autogroup": "Autogroup", + "custom": "Custom", + "user": "User", + "host": "Host", + "group": "Group", + "tag": "Tag", + "srcObject": "Source Object...", + "dstObject": "Destination Object...", + "dstPorts": "Destination Ports...", + "name": "Name:", + "protocol": "Protocol:", + "any": "Any", + "tcp": "TCP", + "udp": "UDP", + "icmp": "ICMP", + "sources": "Sources:", + "destinations": "Destinations:", + "add": "Add", + "policyDeleted": "Policy #{number} deleted", + "sshRuleDeleted": "SSH Rule #{number} deleted", + "sshRule": "SSH Rule", + "usernames": "Usernames:", + "invalidHost": "Invalid host provided", + "invalidPort": "Invalid port number provided", + "tags": "Tags", + "invalidTags": "Invalid Tags:", + "hideInvalid": "Hide Invalid", + "create": "Create", + "ephemeral": "Ephemeral", + "reusable": "Reusable", + "expires": "Expires:", + "used": "Used", + "expired": "Expired", + "delete": "Delete", + "cannotRemoveAllTags": "Cannot remove all tags from a node - tagged nodes must have at least one tag.", + "tagsNotPermitted": "The requested tags are not defined in the ACL policy or are not permitted.", + "taggedToUserReauthRequired": "Headscale 0.28+ does not support moving tagged devices to users directly. Please re-authenticate on the client with 'tailscale up --advertise-tags= --force-reauth'.", + "tagOwnershipWarning": "Adding tags will put this device into 'Tagged Persona' mode. In this mode, the device will no longer belong to a user and will be managed by tags instead. This change is irreversible unless the device is re-authenticated (clear tags and re-auth). Do you want to continue?" + }, + "home": { + "title": "Home", + "totalUsers": "Total Users", + "onlineUsers": "Online Users", + "validPreAuthKeys": "Valid PreAuth Keys", + "totalNodes": "Total Nodes", + "onlineNodes": "Online Nodes", + "totalRoutes": "Total Routes", + "clickToView": "Click to view details" + }, + "status": { + "title": "System Status", + "healthCheck": "System Health Check", + "monitorSystemHealth": "Monitor core system components in real-time", + "databaseConnectivity": "Database Connectivity", + "healthy": "Healthy", + "unhealthy": "Unhealthy", + "lastChecked": "Last Checked", + "systemStatistics": "System Statistics", + "totalUsers": "Total Users", + "totalNodes": "Total Nodes", + "onlineNodes": "Online Nodes", + "offlineNodes": "Offline Nodes", + "totalRoutes": "Total Routes", + "enabledRoutes": "Enabled Routes", + "totalPreAuthKeys": "Total PreAuth Keys", + "validPreAuthKeys": "Valid PreAuth Keys", + "refreshing": "Refreshing...", + "refresh": "Refresh", + "autoRefresh": "Auto-refresh every 30s" + } +} \ No newline at end of file diff --git a/src/lib/locales/zh.json b/src/lib/locales/zh.json new file mode 100644 index 0000000..97ad4db --- /dev/null +++ b/src/lib/locales/zh.json @@ -0,0 +1,337 @@ +{ + "app": { + "title": "Headscale 管理器", + "github": "GitHub" + }, + "navigation": { + "title": "导航", + "home": "首页", + "users": "用户", + "nodes": "节点", + "routes": "路由", + "acls": "访问控制", + "status": "系统状态", + "settings": "设置", + "deploy": "部署" + }, + "settings": { + "title": "设置", + "apiUrl": "API 地址", + "apiKey": "API 密钥", + "apiUrlPlaceholder": "请输入 API 地址", + "apiKeyPlaceholder": "请输入您的 API 密钥", + "apiTtl": "API 刷新间隔(秒)", + "theme": "主题", + "language": "语言", + "debugging": "控制台调试", + "save": "保存设置", + "authorized": "已授权", + "notAuthorized": "未授权", + "expiresIn": "过期时间", + "checkingAuth": "正在检查授权...", + "savedSettings": "设置已保存", + "logUsers": "记录用户", + "logNodes": "记录节点", + "logPreAuthKeys": "记录预授权密钥", + "logApiKeyInfo": "记录 API 密钥信息", + "showApiKey": "显示 API 密钥", + "hideApiKey": "隐藏 API 密钥", + "refreshApiKey": "刷新 API 密钥", + "rememberMe": "记住密钥(保存到本地存储)", + "apiKeysManagement": "API 密钥管理", + "apiKeysList": "API 密钥列表", + "createNewApiKey": "创建新密钥", + "expirationDays": "过期时间(天)", + "neverExpire": "永不过期", + "prefix": "前缀", + "createdAt": "创建时间", + "lastSeen": "最后使用", + "expiration": "过期时间", + "deleteApiKeyConfirm": "确定要删除此 API 密钥吗?", + "deleteApiKeyWarning": "此操作无法撤销。使用此密钥的所有服务将失去访问权限。", + "deletingCurrentKey": "警告:您正在删除当前正在使用的 API 密钥!", + "apiKeyCreated": "API 密钥创建成功", + "apiKeyDeleted": "API 密钥已删除", + "loadingApiKeys": "加载密钥列表中...", + "noApiKeys": "未找到 API 密钥", + "unauthorizedMessage": "API 密钥未授权或无效", + "expiringSoonMessage": "API 密钥即将过期" + }, + "common": { + "close": "关闭", + "cancel": "取消", + "confirm": "确认", + "delete": "删除", + "edit": "编辑", + "save": "保存", + "create": "创建", + "loading": "加载中...", + "copyFailed": "复制到剪贴板失败!", + "copySuccess": "已复制到剪贴板!", + "name": "名称", + "email": "邮箱", + "user": "用户", + "node": "节点", + "route": "路由", + "status": "状态", + "actions": "操作", + "online": "在线", + "offline": "离线", + "expired": "已过期", + "active": "激活", + "createdAt": "创建时间", + "lastSeen": "最后活跃", + "hostname": "主机名", + "addresses": "地址", + "tags": "标签", + "owner": "所有者", + "routes": "路由", + "all": "全部", + "id": "ID", + "sort": "排序", + "filter": "筛选", + "search": "搜索...", + "createUser": "创建用户", + "createNode": "创建节点", + "newUsername": "新用户名...", + "deviceKey": "设备密钥...", + "usernameAndKeyRequired": "用户名和设备密钥是必需的", + "createdUser": "已创建用户", + "createdNode": "已创建节点", + "failedCreateUser": "创建用户失败", + "failedCreateNode": "创建节点失败", + "export": "导出", + "exportData": "导出数据", + "exportFormat": "格式", + "exportResources": "要导出的资源", + "exportIncludeMetadata": "包含元数据", + "exportSuccess": "数据导出成功", + "exportFailed": "导出数据失败", + "exportAll": "导出全部", + "users": "用户", + "nodes": "节点", + "preAuthKeys": "预授权密钥", + "apiKeys": "API 密钥", + "policy": "策略", + "batchOperations": "批量操作", + "selectAll": "全选", + "deselectAll": "取消全选", + "selectedCount": "已选择 {count} 项", + "batchDelete": "批量删除", + "batchSetTags": "批量设置标签", + "batchDeleteConfirm": "确认批量删除", + "batchDeleteWarning": "确定要删除 {count} 个节点吗?此操作无法撤销。", + "batchDeleted": "成功删除 {count} 个节点", + "enterTags": "输入标签(逗号分隔)", + "tagsUpdated": "已更新 {count} 个节点的标签", + "backfillNodeIPs": "回填节点 IP", + "backfillNodeIPsDesc": "为尚未分配 IP 地址的节点分配地址", + "backfillConfirmDesc": "仅补齐缺失 IP,不会修改已有节点 IP。请确认下面的变更列表后再应用。", + "dryRun": "预览", + "applyChanges": "应用更改", + "backfillChanges": "将要应用的更改:", + "noChangesNeeded": "无需更改", + "backfillCompleted": "回填完成:应用了 {count} 项更改" + }, + "status": { + "title": "系统状态", + "healthCheck": "系统运行检查", + "monitorSystemHealth": "实时监控系统核心组件运行状态", + "databaseConnectivity": "数据库连接", + "healthy": "健康", + "unhealthy": "异常", + "lastChecked": "最后检查时间", + "systemStatistics": "系统统计", + "totalUsers": "用户总数", + "totalNodes": "节点总数", + "onlineNodes": "在线节点", + "offlineNodes": "离线节点", + "totalRoutes": "路由总数", + "enabledRoutes": "已启用路由", + "totalPreAuthKeys": "预授权密钥总数", + "validPreAuthKeys": "有效预授权密钥", + "refreshing": "刷新中...", + "refresh": "刷新", + "autoRefresh": "每30秒自动刷新" + }, + "users": { + "title": "用户", + "createUser": "创建用户", + "displayName": "显示名称", + "provider": "提供商", + "email": "邮箱", + "nodes": "节点", + "preAuthKeys": "预授权密钥" + }, + "nodes": { + "title": "节点", + "createNode": "创建节点", + "registrationMethod": "注册方式", + "expiresAt": "过期时间", + "info": "信息" + }, + "routes": { + "title": "路由", + "enabled": "已启用", + "advertised": "已通告", + "destination": "目标" + }, + "acls": { + "title": "访问控制列表", + "config": "配置", + "groups": "组", + "hosts": "主机", + "policies": "策略", + "sshRules": "SSH 规则", + "tagOwners": "标签", + "hostCreated": "主机 '{name}' 已创建", + "tagCreated": "标签 '{name}' 已创建", + "sshRuleCreated": "已创建 SSH 规则 #{count}", + "policyCreated": "已创建策略 #{count}", + "groupCreated": "工作组 '{name}' 已创建", + "configLoaded": "已从服务器加载 ACL 策略", + "configLoadFailed": "无法从服务器获取 ACL 策略", + "configSaved": "已保存 ACL 配置", + "createHost": "创建主机", + "createTag": "创建标签", + "createSshRule": "创建 SSH 规则", + "createPolicy": "创建策略", + "createGroup": "创建工作组", + "saveConfig": "保存配置", + "loadConfig": "加载配置", + "editConfig": "编辑配置", + "applyConfig": "应用配置", + "cancelEditing": "取消编辑", + "resetConfig": "重置配置" + }, + "deploy": { + "title": "部署", + "copyCommand": "命令已复制到剪贴板!", + "general": "常规设置:", + "advertise": "通告设置:", + "accept": "接受设置:", + "saveDefaults": "保存默认设置", + "savedDefaults": "部署默认设置已保存", + "shieldsUp": "防护模式", + "shieldsUpHelp": "阻止传入连接", + "generateQR": "生成二维码", + "generateQRHelp": "创建可扫描的二维码以导入到 TailScale 客户端", + "reset": "重置", + "resetHelp": "将未指定的设置重置为默认值", + "operator": "操作用户", + "operatorHelp": "(仅限 Unix)以不同用户身份运行", + "forceReauth": "强制重新认证", + "forceReauthHelp": "强制用户重新向 Headscale 服务器认证", + "sshServer": "SSH 服务器", + "sshServerHelp": "运行管理员可访问的本地 SSH 服务器", + "preAuthKey": "预授权密钥", + "preAuthKeyHelp": "自动为指定用户认证节点的生成密钥", + "unattended": "无人值守", + "unattendedHelp": "以无人值守模式运行 tailscale 客户端(开机启动)", + "allowLANAccess": "允许局域网访问", + "allowLANAccessHelp": "连接到 TailNet 并使用出口节点时允许本地网络访问", + "advertiseExitNode": "通告出口节点", + "advertiseExitNodeHelp": "允许 TailNet 上的其他节点使用此节点作为网关", + "advertiseTags": "通告标签", + "advertiseTagsHelp": "在配置时应用到设备的通告标签列表", + "advertiseRoutes": "通告路由", + "advertiseRoutesHelp": "可通过此节点访问的子网列表", + "acceptDNS": "接受 DNS", + "acceptDNSHelp": "接受 HeadScale 提供的 DNS 设置", + "acceptRoutes": "接受路由", + "acceptRoutesHelp": "接受其他节点通告的子网", + "exitNode": "出口节点", + "exitNodeHelp": "使用此节点作为网关(目标节点必须通告出口节点)", + "validKeys": "个有效密钥", + "tagError": "标签应为小写字母数字单词", + "cidrError": "无效的 CIDR 格式" + }, + "home": { + "title": "首页", + "totalUsers": "总用户数", + "onlineUsers": "在线用户", + "validPreAuthKeys": "有效预授权密钥", + "totalNodes": "总节点数", + "onlineNodes": "在线节点", + "totalRoutes": "总路由数", + "clickToView": "点击查看详细信息" + }, + "cards": { + "autogroup": "自动组", + "provider": "提供商:", + "email": "邮箱:", + "displayName": "显示名称:", + "hostname": "主机名:", + "created": "创建时间:", + "lastSeen": "最后活跃:", + "user": "用户:", + "routes": "路由:", + "owner": "所有者:", + "tags": "标签:", + "registerMethod": "注册方式:", + "expires": "过期时间:", + "name": "名称:", + "info": "信息:", + "ipv4Address": "IPv4 地址:", + "onlineNow": "在线", + "unspecified": "未指定", + "local": "本地", + "preAuthKey": "预授权密钥", + "cli": "命令行", + "oidc": "OIDC", + "custom": "自定义", + "group": "组", + "host": "主机", + "tag": "标签", + "srcObject": "源对象...", + "dstObject": "目标对象...", + "dstPorts": "目标端口...", + "selectOwners": "选择 {tagName} 的所有者...", + "selectMembers": "选择 {groupName} 的成员...", + "invalidHost": "提供的主机无效", + "invalidPort": "提供的端口号无效", + "protocol": "协议:", + "any": "任意", + "tcp": "TCP", + "udp": "UDP", + "icmp": "ICMP", + "sources": "源:", + "destinations": "目标:", + "add": "添加", + "newOwner": "新所有者:", + "userNameEmpty": "用户名不能为空", + "nodeNameEmpty": "节点名不能为空", + "sshRule": "SSH 规则", + "usernames": "用户名:", + "advertisedTags": "通告标签:", + "tagsPreventedByACL": "以下标签已被当前 ACL 阻止:", + "hideInvalid": "隐藏无效", + "create": "创建", + "ephemeral": "临时", + "reusable": "可重用", + "delete": "删除", + "hostRenameWarning": "主机 '{hostName}' 与用户同名。
请重命名该主机。", + "deletedUser": "已删除用户 \"{name}\" (ID: {id})", + "deletedMachine": "已删除机器 \"{name}\" ({id})", + "failedDeleteUser": "删除用户 \"{name}\" ({id}) 失败", + "failedDeleteMachine": "删除机器 \"{name}\" ({id}) 失败", + "stillHasNodes": " 仍有节点。", + "invalidTags": "无效标签:", + "cannotRemoveAllTags": "无法移除节点的所有标签 - 已打标签的节点必须至少拥有一个标签。", + "tagsNotPermitted": "请求的标签在 ACL 策略中未定义或不被允许。", + "taggedToUserReauthRequired": "Headscale 0.28+ 不支持直接将标签设备移动给用户。请在客户端运行 'tailscale up --advertise-tags= --force-reauth' 重新认证。", + "tagOwnershipWarning": "添加标签将使该设备进入 '标签模式' (Tagged Persona)。在此模式下,设备将脱离用户归属,变更为由标签管理。此操作在 Headscale 0.28+ 中对于已标记的设备是不可逆的归属变更,除非重新验证设备(清除标签并重新认证)。是否继续?", + "policyDeleted": "策略 #{number} 已删除", + "sshRuleDeleted": "SSH 规则 #{number} 已删除", + "groupDeleted": "组 '{name}' 已删除", + "tagDeleted": "标签 '{name}' 已删除", + "groupRenamedSuccess": "组已从 '{oldName}' 重命名为 '{newName}'", + "tagRenamedSuccess": "标签已从 '{oldName}' 重命名为 '{newName}'", + "membersOf": "成员", + "ownersOf": "所有者", + "hostDeleted": "主机 '{name}' 已删除", + "nodes": "节点:", + "used": "已使用", + "expired": "已过期" + } +} \ No newline at end of file diff --git a/src/lib/page/PageDrawer.svelte b/src/lib/page/PageDrawer.svelte index b47e7c6..2b41c0c 100644 --- a/src/lib/page/PageDrawer.svelte +++ b/src/lib/page/PageDrawer.svelte @@ -5,12 +5,16 @@ import NodeInfo from '$lib/cards/node/NodeInfo.svelte'; import Navigation from '$lib/Navigation.svelte'; import { App } from '$lib/States.svelte'; + import { _ } from 'svelte-i18n'; const drawerStore = getDrawerStore(); - +
{#if $drawerStore?.id?.startsWith('userDrawer-')} u.id === $drawerStore?.meta.id)?.name ?? 'N/A'}> @@ -23,7 +27,7 @@ {/if} {#if $drawerStore?.id?.startsWith('navDrawer')} - + {/if} diff --git a/src/lib/page/PageHeader.svelte b/src/lib/page/PageHeader.svelte index 6f98cdd..13e13ca 100644 --- a/src/lib/page/PageHeader.svelte +++ b/src/lib/page/PageHeader.svelte @@ -7,6 +7,7 @@ import type { LayoutStyle, Valued } from '$lib/States.svelte'; import { App } from '$lib/States.svelte'; import type { Snippet } from 'svelte'; + import { _ } from 'svelte-i18n'; type PageHeaderProps = { filterString?: string, @@ -15,6 +16,7 @@ layout?: Valued, buttonText?: string, button?: Snippet, + children?: Snippet, } let { @@ -22,8 +24,9 @@ title, show = $bindable(false), layout = $bindable(undefined), - buttonText = 'Create', + buttonText = $_('common.create'), button, + children, }: PageHeaderProps = $props() const layoutCurrent = $derived(layout !== undefined ? layout.value : null) @@ -45,10 +48,9 @@
{title}
{#if layout && layoutCurrent} -
- +
+ App.toggleLayout(layout)} @@ -56,30 +58,39 @@ background="bg-secondary-500" size="sm" /> - +
{/if}
{#if button !== undefined} -
- {#if buttonText !== ""} - - {/if} - {#if filterString !== undefined} - - {/if} +
+
+ {#if buttonText !== ""} + + {/if} + {#if filterString !== undefined} +
+ +
+ {/if} +
+
+ {@render children?.()} +
{/if}
diff --git a/src/lib/parts/BatchOperationsBar.svelte b/src/lib/parts/BatchOperationsBar.svelte new file mode 100644 index 0000000..fc1def2 --- /dev/null +++ b/src/lib/parts/BatchOperationsBar.svelte @@ -0,0 +1,140 @@ + + +{#if selectedNodesArray.length > 0} +
+
+
+ + {$_('common.selectedCount', { values: { count: selectedNodesArray.length } })} + + +
+ + + + +
+ +
+
+ + +
+
+
+{/if} + + diff --git a/src/lib/parts/ExportModal.svelte b/src/lib/parts/ExportModal.svelte new file mode 100644 index 0000000..410a39a --- /dev/null +++ b/src/lib/parts/ExportModal.svelte @@ -0,0 +1,184 @@ + + +{#if show} + + +
{ + if (e.target === e.currentTarget) { + show = false; + onclose?.(); + } + }} + > +
e.stopPropagation()}> +
+

{$_('common.exportData')}

+ +
+ +
+
+ + +
+ +
+

{$_('common.exportResources')}

+
+ + + + +
+
+ +
+ + +
+
+ +
+ + +
+
+
+{/if} diff --git a/src/lib/parts/LoaderModal.svelte b/src/lib/parts/LoaderModal.svelte index 6958dc3..c649a9f 100644 --- a/src/lib/parts/LoaderModal.svelte +++ b/src/lib/parts/LoaderModal.svelte @@ -33,6 +33,8 @@
{title}
{body}